Dave Jarvis' Repositories

git clone https://repo.autonoma.ca/repo/keenwrite.git
A .classpath
1
<?xml version="1.0" encoding="UTF-8"?>
2
<classpath>
3
	<classpathentry kind="src" path="src/main/java">
4
		<attributes>
5
			<attribute name="FROM_GRADLE_MODEL" value="true"/>
6
		</attributes>
7
	</classpathentry>
8
	<classpathentry kind="src" path="src/main/resources">
9
		<attributes>
10
			<attribute name="FROM_GRADLE_MODEL" value="true"/>
11
		</attributes>
12
	</classpathentry>
13
	<classpathentry kind="con" path="org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.8">
14
		<accessrules>
15
			<accessrule kind="accessible" pattern="javafx/**"/>
16
		</accessrules>
17
	</classpathentry>
18
	<classpathentry exported="true" kind="con" path="org.eclipse.buildship.core.gradleclasspathcontainer"/>
19
	<classpathentry kind="output" path="bin"/>
20
</classpath>
121
A .gitattributes
1
# always use LF line endings
2
3
# ALL FILES:
4
#   Normalize line endings to LF on checkin and
5
#   prevent conversion to CRLF when the file is checked out.
6
7
* text eol=lf
8
9
10
# BINARY FILES:
11
#   Disable line ending normalize on checkin.
12
13
*.gif binary
14
*.jar binary
15
*.png binary
16
*.zip binary
117
A .gitignore
1
/bin/
2
/build/
3
/.gradle/
4
/gradle/
5
/.nb-gradle
6
/private
7
.nb-gradle-properties
8
scrivenvar.pro
19
A .idea/.gitignore
1
workspace.xml
12
A .idea/codeStyles/codeStyleConfig.xml
1
1
<component name="ProjectCodeStyleConfiguration">
2
  <state>
3
    <option name="PREFERRED_PROJECT_CODE_STYLE" value="Default" />
4
  </state>
5
</component>
A .idea/compiler.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="CompilerConfiguration">
4
    <bytecodeTargetLevel target="11" />
5
  </component>
6
</project>
A .idea/gradle.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="GradleMigrationSettings" migrationVersion="1" />
4
  <component name="GradleSettings">
5
    <option name="linkedExternalProjectsSettings">
6
      <GradleProjectSettings>
7
        <option name="distributionType" value="LOCAL" />
8
        <option name="externalProjectPath" value="$PROJECT_DIR$" />
9
        <option name="gradleHome" value="/opt/gradle" />
10
        <option name="gradleJvm" value="#JAVA_HOME" />
11
        <option name="modules">
12
          <set>
13
            <option value="$PROJECT_DIR$" />
14
          </set>
15
        </option>
16
      </GradleProjectSettings>
17
    </option>
18
  </component>
19
</project>
A .idea/jarRepositories.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="RemoteRepositoriesConfiguration">
4
    <remote-repository>
5
      <option name="id" value="central" />
6
      <option name="name" value="Maven Central repository" />
7
      <option name="url" value="https://repo1.maven.org/maven2" />
8
    </remote-repository>
9
    <remote-repository>
10
      <option name="id" value="jboss.community" />
11
      <option name="name" value="JBoss Community repository" />
12
      <option name="url" value="https://repository.jboss.org/nexus/content/repositories/public/" />
13
    </remote-repository>
14
    <remote-repository>
15
      <option name="id" value="MavenRepo" />
16
      <option name="name" value="MavenRepo" />
17
      <option name="url" value="https://repo.maven.apache.org/maven2/" />
18
    </remote-repository>
19
    <remote-repository>
20
      <option name="id" value="maven" />
21
      <option name="name" value="maven" />
22
      <option name="url" value="https://oss.sonatype.org/content/repositories/snapshots/" />
23
    </remote-repository>
24
    <remote-repository>
25
      <option name="id" value="BintrayJCenter" />
26
      <option name="name" value="BintrayJCenter" />
27
      <option name="url" value="https://jcenter.bintray.com/" />
28
    </remote-repository>
29
    <remote-repository>
30
      <option name="id" value="maven2" />
31
      <option name="name" value="maven2" />
32
      <option name="url" value="https://nexus.bedatadriven.com/content/groups/public" />
33
    </remote-repository>
34
  </component>
35
</project>
A .idea/misc.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="ExternalStorageConfigurationManager" enabled="true" />
4
  <component name="ProjectRootManager" version="2" languageLevel="JDK_11" default="false" project-jdk-name="14" project-jdk-type="JavaSDK">
5
    <output url="file://$PROJECT_DIR$/out" />
6
  </component>
7
</project>
A .idea/rGraphicsSettings.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="RGraphicsSettings">
4
    <option name="height" value="1600" />
5
    <option name="resolution" value="112" />
6
    <option name="version" value="1" />
7
    <option name="width" value="2560" />
8
  </component>
9
</project>
A .idea/rSettings.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="RSettings">
4
    <option name="interpreterPath" value="/usr/bin/R" />
5
  </component>
6
</project>
A .idea/rpackages.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="RPackageService">
4
    <option name="enabledRepositoryUrls">
5
      <list>
6
        <option value="@CRAN@" />
7
      </list>
8
    </option>
9
  </component>
10
</project>
A .idea/scrivenvar.iml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<module type="JAVA_MODULE" version="4" />
A .idea/uiDesigner.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="Palette2">
4
    <group name="Swing">
5
      <item class="com.intellij.uiDesigner.HSpacer" tooltip-text="Horizontal Spacer" icon="/com/intellij/uiDesigner/icons/hspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
6
        <default-constraints vsize-policy="1" hsize-policy="6" anchor="0" fill="1" />
7
      </item>
8
      <item class="com.intellij.uiDesigner.VSpacer" tooltip-text="Vertical Spacer" icon="/com/intellij/uiDesigner/icons/vspacer.png" removable="false" auto-create-binding="false" can-attach-label="false">
9
        <default-constraints vsize-policy="6" hsize-policy="1" anchor="0" fill="2" />
10
      </item>
11
      <item class="javax.swing.JPanel" icon="/com/intellij/uiDesigner/icons/panel.png" removable="false" auto-create-binding="false" can-attach-label="false">
12
        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3" />
13
      </item>
14
      <item class="javax.swing.JScrollPane" icon="/com/intellij/uiDesigner/icons/scrollPane.png" removable="false" auto-create-binding="false" can-attach-label="true">
15
        <default-constraints vsize-policy="7" hsize-policy="7" anchor="0" fill="3" />
16
      </item>
17
      <item class="javax.swing.JButton" icon="/com/intellij/uiDesigner/icons/button.png" removable="false" auto-create-binding="true" can-attach-label="false">
18
        <default-constraints vsize-policy="0" hsize-policy="3" anchor="0" fill="1" />
19
        <initial-values>
20
          <property name="text" value="Button" />
21
        </initial-values>
22
      </item>
23
      <item class="javax.swing.JRadioButton" icon="/com/intellij/uiDesigner/icons/radioButton.png" removable="false" auto-create-binding="true" can-attach-label="false">
24
        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
25
        <initial-values>
26
          <property name="text" value="RadioButton" />
27
        </initial-values>
28
      </item>
29
      <item class="javax.swing.JCheckBox" icon="/com/intellij/uiDesigner/icons/checkBox.png" removable="false" auto-create-binding="true" can-attach-label="false">
30
        <default-constraints vsize-policy="0" hsize-policy="3" anchor="8" fill="0" />
31
        <initial-values>
32
          <property name="text" value="CheckBox" />
33
        </initial-values>
34
      </item>
35
      <item class="javax.swing.JLabel" icon="/com/intellij/uiDesigner/icons/label.png" removable="false" auto-create-binding="false" can-attach-label="false">
36
        <default-constraints vsize-policy="0" hsize-policy="0" anchor="8" fill="0" />
37
        <initial-values>
38
          <property name="text" value="Label" />
39
        </initial-values>
40
      </item>
41
      <item class="javax.swing.JTextField" icon="/com/intellij/uiDesigner/icons/textField.png" removable="false" auto-create-binding="true" can-attach-label="true">
42
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
43
          <preferred-size width="150" height="-1" />
44
        </default-constraints>
45
      </item>
46
      <item class="javax.swing.JPasswordField" icon="/com/intellij/uiDesigner/icons/passwordField.png" removable="false" auto-create-binding="true" can-attach-label="true">
47
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
48
          <preferred-size width="150" height="-1" />
49
        </default-constraints>
50
      </item>
51
      <item class="javax.swing.JFormattedTextField" icon="/com/intellij/uiDesigner/icons/formattedTextField.png" removable="false" auto-create-binding="true" can-attach-label="true">
52
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1">
53
          <preferred-size width="150" height="-1" />
54
        </default-constraints>
55
      </item>
56
      <item class="javax.swing.JTextArea" icon="/com/intellij/uiDesigner/icons/textArea.png" removable="false" auto-create-binding="true" can-attach-label="true">
57
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
58
          <preferred-size width="150" height="50" />
59
        </default-constraints>
60
      </item>
61
      <item class="javax.swing.JTextPane" icon="/com/intellij/uiDesigner/icons/textPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
62
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
63
          <preferred-size width="150" height="50" />
64
        </default-constraints>
65
      </item>
66
      <item class="javax.swing.JEditorPane" icon="/com/intellij/uiDesigner/icons/editorPane.png" removable="false" auto-create-binding="true" can-attach-label="true">
67
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
68
          <preferred-size width="150" height="50" />
69
        </default-constraints>
70
      </item>
71
      <item class="javax.swing.JComboBox" icon="/com/intellij/uiDesigner/icons/comboBox.png" removable="false" auto-create-binding="true" can-attach-label="true">
72
        <default-constraints vsize-policy="0" hsize-policy="2" anchor="8" fill="1" />
73
      </item>
74
      <item class="javax.swing.JTable" icon="/com/intellij/uiDesigner/icons/table.png" removable="false" auto-create-binding="true" can-attach-label="false">
75
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
76
          <preferred-size width="150" height="50" />
77
        </default-constraints>
78
      </item>
79
      <item class="javax.swing.JList" icon="/com/intellij/uiDesigner/icons/list.png" removable="false" auto-create-binding="true" can-attach-label="false">
80
        <default-constraints vsize-policy="6" hsize-policy="2" anchor="0" fill="3">
81
          <preferred-size width="150" height="50" />
82
        </default-constraints>
83
      </item>
84
      <item class="javax.swing.JTree" icon="/com/intellij/uiDesigner/icons/tree.png" removable="false" auto-create-binding="true" can-attach-label="false">
85
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3">
86
          <preferred-size width="150" height="50" />
87
        </default-constraints>
88
      </item>
89
      <item class="javax.swing.JTabbedPane" icon="/com/intellij/uiDesigner/icons/tabbedPane.png" removable="false" auto-create-binding="true" can-attach-label="false">
90
        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
91
          <preferred-size width="200" height="200" />
92
        </default-constraints>
93
      </item>
94
      <item class="javax.swing.JSplitPane" icon="/com/intellij/uiDesigner/icons/splitPane.png" removable="false" auto-create-binding="false" can-attach-label="false">
95
        <default-constraints vsize-policy="3" hsize-policy="3" anchor="0" fill="3">
96
          <preferred-size width="200" height="200" />
97
        </default-constraints>
98
      </item>
99
      <item class="javax.swing.JSpinner" icon="/com/intellij/uiDesigner/icons/spinner.png" removable="false" auto-create-binding="true" can-attach-label="true">
100
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
101
      </item>
102
      <item class="javax.swing.JSlider" icon="/com/intellij/uiDesigner/icons/slider.png" removable="false" auto-create-binding="true" can-attach-label="false">
103
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="8" fill="1" />
104
      </item>
105
      <item class="javax.swing.JSeparator" icon="/com/intellij/uiDesigner/icons/separator.png" removable="false" auto-create-binding="false" can-attach-label="false">
106
        <default-constraints vsize-policy="6" hsize-policy="6" anchor="0" fill="3" />
107
      </item>
108
      <item class="javax.swing.JProgressBar" icon="/com/intellij/uiDesigner/icons/progressbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
109
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1" />
110
      </item>
111
      <item class="javax.swing.JToolBar" icon="/com/intellij/uiDesigner/icons/toolbar.png" removable="false" auto-create-binding="false" can-attach-label="false">
112
        <default-constraints vsize-policy="0" hsize-policy="6" anchor="0" fill="1">
113
          <preferred-size width="-1" height="20" />
114
        </default-constraints>
115
      </item>
116
      <item class="javax.swing.JToolBar$Separator" icon="/com/intellij/uiDesigner/icons/toolbarSeparator.png" removable="false" auto-create-binding="false" can-attach-label="false">
117
        <default-constraints vsize-policy="0" hsize-policy="0" anchor="0" fill="1" />
118
      </item>
119
      <item class="javax.swing.JScrollBar" icon="/com/intellij/uiDesigner/icons/scrollbar.png" removable="false" auto-create-binding="true" can-attach-label="false">
120
        <default-constraints vsize-policy="6" hsize-policy="0" anchor="0" fill="2" />
121
      </item>
122
    </group>
123
  </component>
124
</project>
A .idea/vcs.xml
1
1
<?xml version="1.0" encoding="UTF-8"?>
2
<project version="4">
3
  <component name="VcsDirectoryMappings">
4
    <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
  </component>
6
</project>
A .project
1
<?xml version="1.0" encoding="UTF-8"?>
2
<projectDescription>
3
	<name>Markdown Writer FX</name>
4
	<comment></comment>
5
	<projects>
6
	</projects>
7
	<buildSpec>
8
		<buildCommand>
9
			<name>org.eclipse.jdt.core.javabuilder</name>
10
			<arguments>
11
			</arguments>
12
		</buildCommand>
13
		<buildCommand>
14
			<name>org.eclipse.buildship.core.gradleprojectbuilder</name>
15
			<arguments>
16
			</arguments>
17
		</buildCommand>
18
	</buildSpec>
19
	<natures>
20
		<nature>org.eclipse.jdt.core.javanature</nature>
21
		<nature>org.eclipse.buildship.core.gradleprojectnature</nature>
22
	</natures>
23
	<filteredResources>
24
		<filter>
25
			<id>1438449113801</id>
26
			<name></name>
27
			<type>26</type>
28
			<matcher>
29
				<id>org.eclipse.ui.ide.multiFilter</id>
30
				<arguments>1.0-projectRelativePath-matches-false-false-build</arguments>
31
			</matcher>
32
		</filter>
33
		<filter>
34
			<id>1438449113801</id>
35
			<name></name>
36
			<type>26</type>
37
			<matcher>
38
				<id>org.eclipse.ui.ide.multiFilter</id>
39
				<arguments>1.0-projectRelativePath-matches-false-false-.gradle</arguments>
40
			</matcher>
41
		</filter>
42
	</filteredResources>
43
</projectDescription>
144
A .settings/org.eclipse.buildship.core.prefs
1
GRADLE_BUILD_COMMANDS=org.eclipse.jdt.core.javabuilder
2
GRADLE_NATURES=org.eclipse.jdt.core.javanature
3
build.commands=org.eclipse.jdt.core.javabuilder
4
connection.arguments=
5
connection.gradle.distribution=GRADLE_DISTRIBUTION(WRAPPER)
6
connection.gradle.user.home=null
7
connection.java.home=null
8
connection.jvm.arguments=
9
connection.project.dir=
10
derived.resources=.gradle,build
11
eclipse.preferences.version=1
12
natures=org.eclipse.jdt.core.javanature
13
project.path=\:
114
A .settings/org.eclipse.core.runtime.prefs
1
eclipse.preferences.version=1
2
line.separator=\n
13
A .settings/org.eclipse.jdt.core.prefs
1
eclipse.preferences.version=1
2
org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
3
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
4
org.eclipse.jdt.core.compiler.compliance=1.8
5
org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
6
org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
7
org.eclipse.jdt.core.compiler.source=1.8
18
A .settings/org.eclipse.jdt.ui.prefs
1
eclipse.preferences.version=1
2
editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
3
org.eclipse.jdt.ui.ignorelowercasenames=true
4
org.eclipse.jdt.ui.importorder=java;javafx;javax;org;com;
5
org.eclipse.jdt.ui.ondemandthreshold=99
6
org.eclipse.jdt.ui.staticondemandthreshold=99
7
sp_cleanup.add_default_serial_version_id=true
8
sp_cleanup.add_generated_serial_version_id=false
9
sp_cleanup.add_missing_annotations=true
10
sp_cleanup.add_missing_deprecated_annotations=true
11
sp_cleanup.add_missing_methods=false
12
sp_cleanup.add_missing_nls_tags=false
13
sp_cleanup.add_missing_override_annotations=true
14
sp_cleanup.add_missing_override_annotations_interface_methods=true
15
sp_cleanup.add_serial_version_id=false
16
sp_cleanup.always_use_blocks=true
17
sp_cleanup.always_use_parentheses_in_expressions=false
18
sp_cleanup.always_use_this_for_non_static_field_access=false
19
sp_cleanup.always_use_this_for_non_static_method_access=false
20
sp_cleanup.convert_functional_interfaces=false
21
sp_cleanup.convert_to_enhanced_for_loop=false
22
sp_cleanup.correct_indentation=false
23
sp_cleanup.format_source_code=false
24
sp_cleanup.format_source_code_changes_only=false
25
sp_cleanup.insert_inferred_type_arguments=false
26
sp_cleanup.make_local_variable_final=false
27
sp_cleanup.make_parameters_final=false
28
sp_cleanup.make_private_fields_final=true
29
sp_cleanup.make_type_abstract_if_missing_method=false
30
sp_cleanup.make_variable_declarations_final=true
31
sp_cleanup.never_use_blocks=false
32
sp_cleanup.never_use_parentheses_in_expressions=true
33
sp_cleanup.on_save_use_additional_actions=true
34
sp_cleanup.organize_imports=false
35
sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
36
sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
37
sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
38
sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
39
sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
40
sp_cleanup.remove_private_constructors=true
41
sp_cleanup.remove_redundant_type_arguments=true
42
sp_cleanup.remove_trailing_whitespaces=true
43
sp_cleanup.remove_trailing_whitespaces_all=true
44
sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
45
sp_cleanup.remove_unnecessary_casts=true
46
sp_cleanup.remove_unnecessary_nls_tags=false
47
sp_cleanup.remove_unused_imports=true
48
sp_cleanup.remove_unused_local_variables=false
49
sp_cleanup.remove_unused_private_fields=true
50
sp_cleanup.remove_unused_private_members=false
51
sp_cleanup.remove_unused_private_methods=true
52
sp_cleanup.remove_unused_private_types=true
53
sp_cleanup.sort_members=false
54
sp_cleanup.sort_members_all=false
55
sp_cleanup.use_anonymous_class_creation=false
56
sp_cleanup.use_blocks=false
57
sp_cleanup.use_blocks_only_for_return_and_throw=false
58
sp_cleanup.use_lambda=true
59
sp_cleanup.use_parentheses_in_expressions=false
60
sp_cleanup.use_this_for_non_static_field_access=false
61
sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
62
sp_cleanup.use_this_for_non_static_method_access=false
63
sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
64
sp_cleanup.use_type_arguments=false
165
A .travis.yml
1
language: java
2
3
jdk:
4
- oraclejdk8
5
6
# enable Java 8u45+, see https://github.com/travis-ci/travis-ci/issues/4042
7
addons:
8
  apt:
9
    packages:
10
      - oracle-java8-installer
11
os:
12
  - linux
13
14
# run in container
15
sudo: false
116
A BUILD.md
1
# Build
2
3
This document describes how to build the application.
4
5
# Requirements
6
7
Download and install the following software packages:
8
9
* [OpenJDK 14](https://openjdk.java.net)
10
* [Gradle 6.4](https://gradle.org/releases)
11
12
# Compile
13
14
Build the application as follows:
15
16
    gradle clean jar
17
18
The application is built.
19
20
# Run
21
22
After the application is compiled, run it as follows:
23
24
    java -jar build/libs/scrivenvar.jar
25
26
On Windows:
27
28
    java -jar build\libs\scrivenvar.jar
29
130
A CREDITS.md
1
# Credits
2
3
* Dave Jarvis: [Scrivenvar](https://github.com/DaveJarvis/scrivenvar/)
4
* Karl Tauber: [Markdown Writer FX](https://github.com/JFormDesigner/markdown-writer-fx)
5
* Tomas Mikula: [RichTextFX](https://github.com/TomasMikula/RichTextFX), [ReactFX](https://github.com/TomasMikula/ReactFX), [WellBehavedFX](https://github.com/TomasMikula/WellBehavedFX), [Flowless](https://github.com/TomasMikula/Flowless), and [UndoFX](https://github.com/TomasMikula/UndoFX)
6
* Mikael Grev: [MigLayout](http://www.miglayout.com/)
7
* Tom Eugelink: [MigPane](https://github.com/mikaelgrev/miglayout/blob/master/javafx/src/main/java/org/tbee/javafx/scene/layout/fxml/MigPane.java)
8
* Vladimir Schneider: [flexmark](https://website.com)
9
* Jens Deters: [FontAwesomeFX](https://bitbucket.org/Jerady/fontawesomefx)
10
* Shy Shalom, Kohei Taketa: [juniversalchardet](https://github.com/takscape/juniversalchardet)
11
* David Croft, [File Preferences](https://github.com/eis/simple-suomi24-java-client/tree/master/src/main/java/net/infotrek/util/prefs)
12
* Alex Bertram, [Renjin](https://www.renjin.org/)
13
* Michael Kay, [XSLT Processor](http://www.saxonica.com/)
14
115
A LICENSE.md
1
# License
2
3
Copyright 2020 White Magic Software, Ltd.
4
All rights reserved.
5
6
Redistribution and use in source and binary forms, with or without
7
modification, are permitted provided that the following conditions are met:
8
9
* Redistributions of source code must retain the above copyright
10
  notice, this list of conditions and the following disclaimer.
11
12
* Redistributions in binary form must reproduce the above copyright
13
  notice, this list of conditions and the following disclaimer in the
14
  documentation and/or other materials provided with the distribution.
15
16
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
127
A R/README.md
1
# R Functions
2
3
Import the files in this directory into the application, which include:
4
5
* pluralize.R
6
* possessive.R
7
8
# pluralize.R
9
10
This file defines a function that implements most of Damian Conway's [An Algorithmic Approach to English Pluralization](http://blob.perl.org/tpc/1998/User_Applications/Algorithmic%20Approach%20Plurals/Algorithmic_Plurals.html).
11
12
## Usage
13
14
Example usages of the pluralize function include:
15
16
    `r#pluralize( 'mouse' )` - mice
17
    `r#pluralize( 'buzz' )` - buzzes
18
    `r#pluralize( 'bus' )` - busses
19
20
# possessive.R
21
22
This file defines a function that applies possessives to English words.
23
24
## Usage
25
26
Example usages of the possessive function include:
27
28
    `r#pos( 'Ross' )` - Ross'
29
    `r#pos( 'Ruby' )` - Ruby's
30
    `r#pos( 'Lois' )` - Lois'
31
    `r#pos( 'my' )` - mine
32
    `r#pos( 'Your' )` - Yours
33
134
A R/pluralize.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
26
#   http://goo.gl/oRL4MP
27
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
28
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
29
# See also: http://www.freevectors.net/assets/files/plural.txt
30
# -----------------------------------------------------------------------------
31
pluralize <- function( s, n ) {
32
  result <- s
33
34
  # Partial implementation of Conway's algorithm for nouns.
35
  if( n != 1 ) {
36
    if( pl.noninflective( s ) ||
37
        pl.suffix( "es", s ) ||
38
        pl.suffix( "fish", s ) ||
39
        pl.suffix( "ois", s ) ||
40
        pl.suffix( "sheep", s ) ||
41
        pl.suffix( "deer", s ) ||
42
        pl.suffix( "pox", s ) ||
43
        pl.suffix( "[A-Z].*ese", s ) ||
44
        pl.suffix( "itis", s ) ) {
45
      # 1. Retain non-inflective user-mapped noun as is.
46
      # 2. Retain non-inflective plural as is.
47
      result <- s
48
    }
49
    else if( pl.is.irregular.pl( s ) ) {
50
      # 4. Change irregular plurals based on mapping.
51
      result <- pl.irregular.pl( s )
52
    }
53
    else if( pl.is.irregular.es( s ) ) {
54
      # x. From Shevek's
55
      result <- pl.inflect( s, "", "es" )
56
    }
57
    else if( pl.suffix( "man", s ) ) {
58
      # 5. For -man, change -an to -en
59
      result <- pl.inflect( s, "an", "en" )
60
    }
61
    else if( pl.suffix( "[lm]ouse", s ) ) {
62
      # 5. For [lm]ouse, change -ouse to -ice
63
      result <- pl.inflect( s, "ouse", "ice" )
64
    }
65
    else if( pl.suffix( "tooth", s ) ) {
66
      # 5. For -tooth, change -ooth to -eeth
67
      result <- pl.inflect( s, "ooth", "eeth" )
68
    }
69
    else if( pl.suffix( "goose", s ) ) {
70
      # 5. For -goose, change -oose to -eese
71
      result <- pl.inflect( s, "oose", "eese" )
72
    }
73
    else if( pl.suffix( "foot", s ) ) {
74
      # 5. For -foot, change -oot to -eet
75
      result <- pl.inflect( s, "oot", "eet" )
76
    }
77
    else if( pl.suffix( "zoon", s ) ) {
78
      # 5. For -zoon, change -on to -a
79
      result <- pl.inflect( s, "on", "a" )
80
    }
81
    else if( pl.suffix( "[csx]is", s ) ) {
82
      # 5. Change -cis, -sis, -xis to -es
83
      result <- pl.inflect( s, "is", "es" )
84
    }
85
    else if( pl.suffix( "([cs]h|ss|zz|x|s)", s ) ) {
86
      # 8. Change -ch, -sh, -ss, -zz, -x, -s to -es
87
      result <- pl.inflect( s, "", "es" )
88
    }
89
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
90
      # 9. Change -f to -ves
91
      result <- pl.inflect( s, "f", "ves" )
92
    }
93
    else if( pl.suffix( "[nlw]ife", s ) ) {
94
      # 10. Change -fe to -ves
95
      result <- pl.inflect( s, "fe", "ves" )
96
    }
97
    else if( pl.suffix( "[aeiou]y", s ) ) {
98
      # 11. Change -[aeiou]y to -ys
99
      result <- pl.inflect( s, "", "s" )
100
    }
101
    else if( pl.suffix( "y", s ) ) {
102
      # 12. Change -y to -ies
103
      result <- pl.inflect( s, "y", "ies" )
104
    }
105
    else if( pl.suffix( "z", s ) ) {
106
      # x. Change -z to -zzes
107
      result <- pl.inflect( s, "", "zes" )
108
    }
109
    else {
110
      # 13. Default plural: add -s
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# -----------------------------------------------------------------------------
119
# Returns the given string (s) with its suffix replaced by r.
120
# -----------------------------------------------------------------------------
121
pl.inflect <- function( s, suffix, r ) {
122
  gsub( paste( suffix, "$", sep="" ), r, s )
123
}
124
125
# -----------------------------------------------------------------------------
126
# Answers whether the given string (s) has the given ending.
127
# -----------------------------------------------------------------------------
128
pl.suffix <- function( ending, s ) {
129
  grepl( paste( ending, "$", sep="" ), s )
130
}
131
132
# -----------------------------------------------------------------------------
133
# Answers whether the given string (s) is a noninflective noun.
134
# -----------------------------------------------------------------------------
135
pl.noninflective <- function( s ) {
136
  v <- c(
137
    "aircraft",
138
    "Bhutanese",
139
    "bison",
140
    "bream",
141
    "Burmese",
142
    "carp",
143
    "chassis",
144
    "Chinese",
145
    "clippers",
146
    "cod",
147
    "contretemps",
148
    "corps",
149
    "debris",
150
    "djinn",
151
    "eland",
152
    "elk",
153
    "flounder",
154
    "fracas",
155
    "gallows",
156
    "graffiti",
157
    "headquarters",
158
    "high-jinks",
159
    "homework",
160
    "hovercraft",
161
    "innings",
162
    "Japanese",
163
    "Lebanese",
164
    "mackerel",
165
    "means",
166
    "mews",
167
    "mice",
168
    "mumps",
169
    "news",
170
    "pincers",
171
    "pliers",
172
    "Portuguese",
173
    "proceedings",
174
    "salmon",
175
    "scissors",
176
    "sea-bass",
177
    "Senegalese",
178
    "shears",
179
    "Siamese",
180
    "Sinhalese",
181
    "spacecraft",
182
    "swine",
183
    "trout",
184
    "tuna",
185
    "Vietnamese",
186
    "watercraft",
187
    "whiting",
188
    "wildebeest"
189
  )
190
191
  is.element( s, v )
192
}
193
194
# -----------------------------------------------------------------------------
195
# Answers whether the given string (s) is an irregular plural.
196
# -----------------------------------------------------------------------------
197
pl.is.irregular.pl <- function( s ) {
198
  # Could be refactored with pl.irregular.pl...
199
  v <- c(
200
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
201
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
202
  )
203
204
  is.element( s, v )
205
}
206
207
# -----------------------------------------------------------------------------
208
# Call to pluralize an irregular noun. Only call after confirming
209
# the noun is irregular via pl.is.irregular.pl.
210
# -----------------------------------------------------------------------------
211
pl.irregular.pl <- function( s ) {
212
  v <- list(
213
    "beef" = "beefs",
214
    "brother" = "brothers",
215
    "child" = "children",
216
    "cow" = "cows",
217
    "ephemeris" = "ephemerides",
218
    "genie" = "genies",
219
    "money" = "moneys",
220
    "mongoose" = "mongooses",
221
    "mythos" = "mythoi",
222
    "octopus" = "octopuses",
223
    "ox" = "oxen",
224
    "soliloquy" = "soliloquies",
225
    "trilby" = "trilbys"
226
  )
227
228
  # Faster version of v[[ s ]]
229
  .subset2( v, s )
230
}
231
232
# -----------------------------------------------------------------------------
233
# Answers whether the given string (s) pluralizes with -es.
234
# -----------------------------------------------------------------------------
235
pl.is.irregular.es <- function( s ) {
236
  v <- c(
237
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
238
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
239
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
240
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
241
    "polis", "rhinoceros", "sassafrass", "trellis"
242
  )
243
244
  is.element( s, v )
245
}
246
1247
A R/possessive.R
1
# -----------------------------------------------------------------------------
2
# Copyright 2020, White Magic Software, Ltd.
3
# 
4
# Permission is hereby granted, free of charge, to any person obtaining
5
# a copy of this software and associated documentation files (the
6
# "Software"), to deal in the Software without restriction, including
7
# without limitation the rights to use, copy, modify, merge, publish,
8
# distribute, sublicense, and/or sell copies of the Software, and to
9
# permit persons to whom the Software is furnished to do so, subject to
10
# the following conditions:
11
# 
12
# The above copyright notice and this permission notice shall be
13
# included in all copies or substantial portions of the Software.
14
# 
15
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
# -----------------------------------------------------------------------------
23
24
# -----------------------------------------------------------------------------
25
# Returns leftmost n characters of s.
26
# -----------------------------------------------------------------------------
27
lstr <- function( s, n = 1 ) {
28
  substr( s, 0, n )
29
}
30
31
# -----------------------------------------------------------------------------
32
# Returns rightmost n characters of s.
33
# -----------------------------------------------------------------------------
34
rstr <- function( s, n = 1 ) {
35
  l <- nchar( s )
36
  substr( s, l - n + 1, l )
37
}
38
39
# -----------------------------------------------------------------------------
40
# Returns the possessive form of the given word, s.
41
# -----------------------------------------------------------------------------
42
pos <- function( s ) {
43
  lcs <- tolower( s )
44
  pronouns <- c( 'your', 'our', 'her', 'it', 'their' )
45
46
  if( lcs == 'my' ) {
47
    # Change "[Mm]y" to "[Mm]ine".
48
    s <- paste0( lstr( s, 1 ), "ine" )
49
  }
50
  else if( lcs %in% pronouns ) {
51
    # Append an s to most pronouns.
52
    s <- paste0( s, 's' )
53
  }
54
  else if( lcs != 'his' ) {
55
    # Possessive for all other words except 'his'.
56
    s <- paste0( s, ifelse( rstr( s, 1 ) == 's', "'" ,"'s" ) )
57
  }
58
59
  s
60
}
61
162
A README.md
1
![Logo](images/logo64.png)
2
3
# $application.title$
4
5
A text editor that uses [interpolated strings](https://en.wikipedia.org/wiki/String_interpolation) to reference externally defined values.
6
7
## Requirements
8
9
Download and install the following software packages:
10
11
* [OpenJDK 14](https://openjdk.java.net)
12
13
## Quick Start
14
15
Complete the following steps to run the application:
16
17
1. [Download](https://github.com/DaveJarvis/scrivenvar/releases)
18
`scrivenvar.jar`.
19
1. Double-click `scrivenvar.jar` to start the application.
20
21
## Command Line Start
22
23
If the quick start fails, run the application as follows:
24
25
1. Open a command prompt.
26
1. Change to the download directory containing the archive file.
27
1. Run: `java -jar scrivenvar.jar`
28
29
## Features
30
31
* R integration
32
* User-defined variables, interpolated
33
* Real-time preview with variable substitution
34
* Auto-complete variable names based on variable values
35
* XML document transformation using XSLT3 or older
36
* Platform independent (Windows, Linux, MacOS)
37
38
## Usage
39
40
See the following documents for more information:
41
42
* [USAGE.md](USAGE.md) - how variable definitions and string interpolation work.
43
* [USAGE-R.md](USAGE-R.md) - how to call R functions in R Markdown documents.
44
45
## Future Features
46
47
* Spell check
48
* Search and replace using variables
49
* Re-organize variable names
50
51
## Screenshot
52
53
![Screenshot](images/screenshot.png)
54
55
## License
56
57
This software is licensed under the [BSD 2-Clause License](LICENSE.md).
58
159
A USAGE-R.md
1
# Introduction
2
3
This document describes how to use the [R](https://www.r-project.org/)
4
programming language from within the application. The application uses an
5
interpreter known as [Renjin](https://www.renjin.org/) to integrate with R.
6
7
# Hello World
8
9
Complete the following steps to see R in action:
10
11
1. Start the application.
12
1. Click **File → New** to create a new file.
13
1. Click **File → Save As**.
14
1. Set **Name** to: `addition.Rmd`
15
1. Click **Save**.
16
17
Setting the file name extension tells the application what processor to
18
use when transforming the contents for display in the preview pane. Continue
19
by typing in the following text, including the backticks:
20
21
```r
22
`r#1 + 1`
23
```
24
25
The preview pane shows the result of `1` plus `1`:
26
27
```
28
2.0
29
```
30
31
# Bootstrap Script
32
33
Being able to run R code while editing an R Markdown document is convenient.
34
Having the ability to call functions is where the power of R can be
35
leveraged.
36
37
Complete the following steps to call an R function from your own library:
38
39
1. Click **File → New** to create a new file.
40
1. Click **File → Save As**.
41
1. Browse to your home directory.
42
1. Set **Name** to: `library.R`.
43
1. Click **Save**.
44
1. Set the contents to:
45
    ``` r
46
    sum <- function( a, b ) {
47
      a + b
48
    }
49
    ```
50
1. Click the **Save** icon.
51
1. Click **R → Script**.
52
1. Set the **R Startup Script** contents to:
53
    ``` r
54
    source( 'library.R' );
55
    ```
56
1. Click **OK**.
57
1. Create a new file.
58
1. Set the contents to:
59
    ``` r
60
    `r#sum( 5, 5 )`
61
    ```
62
1. Save the file as `sum.R`.
63
64
The preview panel shows the result of calling the `sum` function:
65
66
```
67
10.0
68
```
69
70
This shows how the bootstrap script can load `library.R`, which defines
71
a `sum` function that is called by name in the Markdown document.
72
73
# Working Directory
74
75
R files may be sourced from any directory, not just the user's home
76
directory. Accomplish this as follows:
77
78
1. Click **R → Directory**.
79
1. Set **Directory** to a different directory.
80
1. Click **OK**.
81
1. Create the directory if it does not exist.
82
1. Move `library.R` into the directory.
83
1. Append a new function to `library.R` as follows:
84
    ``` r
85
    mul <- function( a, b ) {
86
      a * b
87
    }
88
    ```
89
1. Click **R → Script**.
90
1. Set the **R Startup Script** contents to:
91
    ``` r
92
    setwd( '$application.r.working.directory$' );
93
    source( 'library.R' );
94
    ```
95
1. Change `sum.Rmd` to:
96
    ``` r
97
    `r#mul( 5, 5 )`
98
    ```
99
1. Close the file `sum.Rmd`.
100
1. Confirm saving the file when prompted.
101
1. Re-open `sum.Rmd`.
102
103
The preview panel shows:
104
105
```
106
25.0
107
```
108
109
Calling `setwd` using `'$application.r.working.directory$'` changes the
110
working directory where the R engine searches for source files.
111
112
# YAML Definitions
113
114
To see how variable definitions work in R, try the following:
115
116
1. Create a new file.
117
1. Change the contents to (use spaces not tabs):
118
    ``` yaml
119
    project:
120
      title: Project Title
121
      author: Author Name
122
    ```
123
1. Save the file as `definitions.yaml`.
124
1. Click **File → Open**.
125
1. Set **Source Files** to **Definition Files**.
126
1. Select `definitions.yaml`.
127
1. Click **Open**.
128
1. Open `sum.Rmd` if it is not already open.
129
1. Type: `je`
130
1. Press `Ctrl+Space`
131
132
The editor inserts the following text (matches `je` against Pro**je**ct):
133
134
``` r
135
`r#x( v$project$title )`
136
```
137
138
The preview panel shows:
139
140
```
141
r#x( 'Project Title' )
142
```
143
144
This is because the application inserts definition reference names based
145
on the type of file being edited. By default, the R engine does not have
146
a function named `x` defined.
147
148
Continue as follows:
149
150
1. Click **R → Script**.
151
1. Append the following:
152
    ``` r
153
    x <- function( s ) {
154
      tryCatch( {
155
        r = eval( parse( text = s ) )
156
157
        ifelse( is.atomic( r ), r, s );
158
      },
159
      warning = function( w ) { s },
160
      error = function( e ) { s } )
161
    }
162
    ```
163
1. Click **OK**.
164
1. Close and re-open `sum.Rmd`.
165
166
The preview panel shows:
167
168
```
169
25.0
170
171
Project Title
172
```
173
174
The `x` function attempts to evaluate the expression defined by the YAML
175
variable. This means that the YAML definitions can also include expressions
176
that R is capable of evaluating.
177
178
While the `x` function can be defined within the R Startup Script, it is
179
better practice to put it into its own library so that it can be reused
180
outside of the application.
181
1182
A USAGE.md
1
# Introduction
2
3
This document describes how to use the application.
4
5
# Variable Definitions
6
7
Variable definitions provide a way to insert key names having associated values into a document. The variable names and values are declared inside an external file using the [YAML](http://www.yaml.org/) file format. Simply put, variables are written in the file as follows:
8
9
```
10
key: value
11
```
12
13
Any number of variables can be defined, in any order:
14
15
```
16
key_1: Value 1
17
key_2: Value 2
18
```
19
20
Variables can reference other variables by enclosing the key name within dollar symbols:
21
22
```
23
key: Value
24
key_1: $key$ 1
25
key_2: $key$ 2
26
```
27
28
Variables can use a nested structure to help group related information:
29
30
```
31
novel:
32
  title: Book Title
33
  author: Author Name
34
  isbn: 978-3-16-148410-0
35
```
36
37
Use a period to reference nested keys, such as:
38
39
```
40
novel:
41
  author: Author Name
42
copyright:
43
  owner: $novel.author$
44
```
45
46
Save the variable definitions in a file having an extension of `.yaml` or `.yml`.
47
48
# Document Editing
49
50
The application's purpose is to completely separate the document's content from its presentation. To achieve this, documents are composed using a [plain text](http://spec.commonmark.org/0.28/) format.
51
52
## Create Document
53
54
Start a new document as follows:
55
56
1. Start the application.
57
1. Click **File → New** to create an empty document to edit.
58
1. Click **File → Open** to open a variable definition file.
59
1. Change **Source Files** to **Definition Files** to list definition files.
60
1. Browse to and select a file saved with a `.yaml` or `.yml` extension.
61
1. Click **Open**.
62
63
The variable definitions appear in the variable definition pane under the heading of **Definitions**.
64
65
## Edit Document
66
67
Edit the document as normal. Notice how the preview pane updates as new content is added. The toolbar shows various icons that perform different formatting operations. Try them to see how they appear in the preview pane. Other operations not shown on the toolbar include:
68
69
* Struck text (enclose the words within `~~` and `~~`)
70
* Horizontal rule (use `---` on an otherwise empty line).
71
72
The preview pane shows one way to interpret and format the document, but many other presentations are possible.
73
74
## Insert Variable
75
76
Let's assume that the variable definitions loaded into the application include:
77
78
```
79
novel:
80
  title: Diary of $novel.author$
81
  author: Anne Frank
82
```
83
84
To reference a variable, type in the key name enclosed within dollar symbols, such as:
85
86
```
87
The novel "$novel.title$" is one of the most widely read books in the world.
88
```
89
90
The preview pane shows:
91
92
> The novel "Diary of Anne Frank" is one of the most widely read books in the world.
93
94
As it is laborious to type in variable names, it is possible to inject the variable name using autocomplete. Accomplish this as follows:
95
96
1. Create a new file.
97
1. Type in a partial variable value, such as **Dia**.
98
1. Press `Ctrl+Space` (hold down the `Control` key and tap the spacebar).
99
100
The editor shows:
101
102
```
103
$novel.title$
104
```
105
106
The preview pane shows:
107
108
```
109
Diary of Anne Frank
110
```
111
112
The variable name is inserted into the document and the preview pane shows the variable's value.
113
1114
A _config.yaml
1
---
2
application:
3
  title: "Scrivenvar"
14
A build.gradle
1
plugins {
2
  id 'application'
3
  id 'org.openjfx.javafxplugin' version '0.0.8'
4
  id 'com.palantir.git-version' version '0.12.3'
5
}
6
7
repositories {
8
  mavenCentral()
9
  jcenter()
10
11
  maven {
12
    url 'https://oss.sonatype.org/content/repositories/snapshots/'
13
  }
14
15
  maven {
16
    url "https://nexus.bedatadriven.com/content/groups/public"
17
  }
18
}
19
20
dependencies {
21
  implementation 'org.controlsfx:controlsfx:11.0.1'
22
  implementation 'org.fxmisc.richtext:richtextfx:0.10.5'
23
  implementation 'org.fxmisc.wellbehaved:wellbehavedfx:0.3.3'
24
  implementation 'com.miglayout:miglayout-javafx:5.2'
25
  implementation 'com.dlsc.preferencesfx:preferencesfx-core:11.6.0'
26
  implementation 'com.vladsch.flexmark:flexmark:0.62.2'
27
  implementation 'com.vladsch.flexmark:flexmark-ext-tables:0.62.2'
28
  implementation 'com.vladsch.flexmark:flexmark-ext-superscript:0.62.2'
29
  implementation 'com.vladsch.flexmark:flexmark-ext-gfm-strikethrough:0.62.2'
30
  implementation 'com.fasterxml.jackson.core:jackson-core:2.11.0'
31
  implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.0'
32
  implementation 'com.fasterxml.jackson.core:jackson-annotations:2.11.0'
33
  implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.11.0'
34
  implementation 'org.ahocorasick:ahocorasick:0.4.0'
35
  implementation 'org.yaml:snakeyaml:1.26'
36
  implementation 'com.ximpleware:vtd-xml:2.13.4'
37
  implementation 'net.sf.saxon:Saxon-HE:10.1'
38
  implementation 'org.apache.commons:commons-configuration2:2.7'
39
  implementation 'com.googlecode.juniversalchardet:juniversalchardet:1.0.3'
40
  implementation 'de.jensd:fontawesomefx-commons:11.0'
41
  implementation 'de.jensd:fontawesomefx-fontawesome:4.7.0-11'
42
  implementation 'org.renjin:renjin-script-engine:3.5-beta76'
43
  implementation 'org.xhtmlrenderer:flying-saucer-core:9.1.20'
44
  implementation 'org.jsoup:jsoup:1.13.1'
45
  implementation 'org.apache.xmlgraphics:batik-all:1.13'
46
47
  def os = ['win', 'linux', 'mac']
48
  def fx = ['controls', 'graphics', 'fxml', 'swing']
49
50
  fx.each { fxitem ->
51
    os.each { ositem ->
52
      runtimeOnly "org.openjfx:javafx-${fxitem}:${javafx.version}:${ositem}"
53
    }
54
  }
55
56
  testImplementation('org.junit.jupiter:junit-jupiter-api:5.4.2')
57
  testRuntime('org.junit.jupiter:junit-jupiter-engine:5.4.2')
58
}
59
60
javafx {
61
  version = "14"
62
  modules = ['javafx.controls', 'javafx.graphics', 'javafx.swing']
63
}
64
65
compileJava {
66
  options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation"
67
}
68
69
sourceCompatibility = JavaVersion.VERSION_11
70
applicationName = 'scrivenvar'
71
version = gitVersion()
72
mainClassName = "com.${applicationName}.Main"
73
def launcherClassName = "com.${applicationName}.Launcher"
74
75
def propertiesFile = new File("src/main/resources/com/${applicationName}/app.properties")
76
propertiesFile.write("application.version=${version}")
77
78
jar {
79
  duplicatesStrategy = DuplicatesStrategy.EXCLUDE
80
81
  manifest {
82
    attributes 'Main-Class': launcherClassName
83
  }
84
85
  from {
86
    (configurations.runtimeClasspath.findAll { !it.path.endsWith(".pom") }).collect {
87
      it.isDirectory() ? it : zipTree(it)
88
    }
89
  }
90
91
  archiveFileName = "${applicationName}.jar"
92
93
  exclude 'META-INF/*.RSA', 'META-INF/*.SF', 'META-INF/*.DSA'
94
}
95
96
distributions {
97
  main {
98
    distributionBaseName = applicationName
99
    contents {
100
      from { ['LICENSE.md', 'README.md'] }
101
      into('images') {
102
        from { 'images' }
103
      }
104
    }
105
  }
106
}
107
108
test {
109
  useJUnitPlatform()
110
}
1111
A gradle.properties
1
org.gradle.jvmargs=-Xmx1G -XX:MaxPermSize=512m
2
13
A images/logo64.png
Binary file
A images/screenshot.png
Binary file
A licenses/MARKDOWN-WRITER-FX.md
1
Copyright (c) 2015 Karl Tauber <karl@jformdesigner.com>
2
All rights reserved.
3
4
Redistribution and use in source and binary forms, with or without
5
modification, are permitted provided that the following conditions are met:
6
7
* Redistributions of source code must retain the above copyright
8
  notice, this list of conditions and the following disclaimer.
9
10
* Redistributions in binary form must reproduce the above copyright
11
  notice, this list of conditions and the following disclaimer in the
12
  documentation and/or other materials provided with the distribution.
13
14
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
125
A settings.gradle
11
A src/main/java/com/scrivenvar/AbstractFileFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.predicates.files.FileTypePredicate;
31
import com.scrivenvar.service.Settings;
32
33
import java.nio.file.Path;
34
import java.util.Iterator;
35
import java.util.List;
36
37
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
38
import static java.lang.String.format;
39
40
/**
41
 * Provides common behaviours for factories that instantiate classes based on
42
 * file type.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class AbstractFileFactory {
47
48
  private static final String MSG_UNKNOWN_FILE_TYPE =
49
      "Unknown type '%s' for file '%s'.";
50
51
  private final Settings mSettings = Services.load( Settings.class );
52
53
  /**
54
   * Determines the file type from the path extension. This should only be
55
   * called when it is known that the file type won't be a definition file
56
   * (e.g., YAML or other definition source), but rather an editable file
57
   * (e.g., Markdown, XML, etc.).
58
   *
59
   * @param path The path with a file name extension.
60
   * @return The FileType for the given path.
61
   */
62
  public FileType lookup( final Path path ) {
63
    return lookup( path, GLOB_PREFIX_FILE );
64
  }
65
66
  /**
67
   * Creates a file type that corresponds to the given path.
68
   *
69
   * @param path   Reference to a variable definition file.
70
   * @param prefix One of GLOB_PREFIX_DEFINITION or GLOB_PREFIX_FILE.
71
   * @return The file type that corresponds to the given path.
72
   */
73
  protected FileType lookup( final Path path, final String prefix ) {
74
    assert path != null;
75
    assert prefix != null;
76
77
    final Settings properties = getSettings();
78
    final Iterator<String> keys = properties.getKeys( prefix );
79
80
    boolean found = false;
81
    FileType fileType = null;
82
83
    while( keys.hasNext() && !found ) {
84
      final String key = keys.next();
85
      final List<String> patterns = properties.getStringSettingList( key );
86
      final FileTypePredicate predicate = new FileTypePredicate( patterns );
87
88
      if( found = predicate.test( path.toFile() ) ) {
89
        // Remove the EXTENSIONS_PREFIX to get the filename extension mapped
90
        // to a standard name (as defined in the settings.properties file).
91
        final String suffix = key.replace( prefix + ".", "" );
92
        fileType = FileType.from( suffix );
93
      }
94
    }
95
96
    return fileType;
97
  }
98
99
  /**
100
   * Throws IllegalArgumentException because the given path could not be
101
   * recognized. This exists because
102
   *
103
   * @param type The detected path type (protocol, file extension, etc.).
104
   * @param path The path to a source of definitions.
105
   */
106
  protected void unknownFileType( final String type, final String path ) {
107
    final String msg = format( MSG_UNKNOWN_FILE_TYPE, type, path );
108
    throw new IllegalArgumentException( msg );
109
  }
110
111
  /**
112
   * Return the singleton Settings instance.
113
   *
114
   * @return A non-null instance.
115
   */
116
  private Settings getSettings() {
117
    return this.mSettings;
118
  }
119
}
1120
A src/main/java/com/scrivenvar/AbstractPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import org.tbee.javafx.scene.layout.fxml.MigPane;
31
32
/**
33
 * Hides dependency on {@link MigPane} from subclasses.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class AbstractPane extends MigPane {
38
}
139
A src/main/java/com/scrivenvar/Constants.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.service.Settings;
31
32
/**
33
 * Defines application-wide default values.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public class Constants {
38
39
  private static final Settings SETTINGS = Services.load( Settings.class );
40
41
  /**
42
   * Prevent instantiation.
43
   */
44
  private Constants() {
45
  }
46
47
  private static String get( final String key ) {
48
    return SETTINGS.getSetting( key, "" );
49
  }
50
51
  @SuppressWarnings("SameParameterValue")
52
  private static int get( final String key, final int defaultValue ) {
53
    return SETTINGS.getSetting( key, defaultValue );
54
  }
55
56
  // Bootstrapping...
57
  public static final String SETTINGS_NAME =
58
      "/com/scrivenvar/settings.properties";
59
60
  public static final String APP_TITLE = get( "application.title" );
61
  public static final String APP_BUNDLE_NAME = get( "application.messages" );
62
63
  // Prevent double events when updating files on Linux (save and timestamp).
64
  public static final int APP_WATCHDOG_TIMEOUT = get(
65
      "application.watchdog.timeout", 200 );
66
67
  public static final String STYLESHEET_SCENE = get( "file.stylesheet.scene" );
68
  public static final String STYLESHEET_MARKDOWN = get(
69
      "file.stylesheet.markdown" );
70
  public static final String STYLESHEET_PREVIEW = get(
71
      "file.stylesheet.preview" );
72
73
  public static final String FILE_LOGO_16 = get( "file.logo.16" );
74
  public static final String FILE_LOGO_32 = get( "file.logo.32" );
75
  public static final String FILE_LOGO_128 = get( "file.logo.128" );
76
  public static final String FILE_LOGO_256 = get( "file.logo.256" );
77
  public static final String FILE_LOGO_512 = get( "file.logo.512" );
78
79
  public static final String PREFS_ROOT = get( "preferences.root" );
80
  public static final String PREFS_STATE = get( "preferences.root.state" );
81
82
  // Refer to filename extension settings in the configuration file. Do not
83
  // terminate these prefixes with a period.
84
  public static final String GLOB_PREFIX_FILE = "file.ext";
85
  public static final String GLOB_PREFIX_DEFINITION =
86
      "definition." + GLOB_PREFIX_FILE;
87
88
  // Different definition source protocols.
89
  public static final String DEFINITION_PROTOCOL_UNKNOWN = "unknown";
90
  public static final String DEFINITION_PROTOCOL_FILE = "file";
91
92
  // Takes two parameters: line number and column number.
93
  public static final String STATUS_BAR_LINE = "Main.statusbar.line";
94
95
  // "OK" text
96
  public static final String STATUS_BAR_OK = "Main.statusbar.state.default";
97
  public static final String STATUS_PARSE_ERROR = "Main.statusbar.parse.error";
98
99
  /**
100
   * Used when creating flat maps relating to resolved variables.
101
   */
102
  public static final int DEFAULT_MAP_SIZE = 64;
103
104
  public static final String PERSIST_IMAGES_DEFAULT =
105
      get( "file.stylesheet.scene" );
106
107
  /**
108
   * Default working directory to use for R startup script.
109
   */
110
  public static final String USER_DIRECTORY = System.getProperty( "user.dir" );
111
}
1112
A src/main/java/com/scrivenvar/FileEditorTab.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * Redistribution and use in source and binary forms, with or without
5
 * modification, are permitted provided that the following conditions are met:
6
 *
7
 *  o Redistributions of source code must retain the above copyright
8
 *    notice, this list of conditions and the following disclaimer.
9
 *
10
 *  o Redistributions in binary form must reproduce the above copyright
11
 *    notice, this list of conditions and the following disclaimer in the
12
 *    documentation and/or other materials provided with the distribution.
13
 *
14
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
15
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
16
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
17
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
18
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
19
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
20
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
21
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
22
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
24
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
 */
26
package com.scrivenvar;
27
28
import com.scrivenvar.editors.EditorPane;
29
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
30
import com.scrivenvar.service.events.Notification;
31
import com.scrivenvar.service.events.Notifier;
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.BooleanProperty;
35
import javafx.beans.property.ReadOnlyBooleanProperty;
36
import javafx.beans.property.ReadOnlyBooleanWrapper;
37
import javafx.beans.property.SimpleBooleanProperty;
38
import javafx.beans.value.ChangeListener;
39
import javafx.beans.value.ObservableValue;
40
import javafx.event.Event;
41
import javafx.event.EventHandler;
42
import javafx.event.EventType;
43
import javafx.scene.Node;
44
import javafx.scene.Scene;
45
import javafx.scene.control.Tab;
46
import javafx.scene.control.Tooltip;
47
import javafx.scene.text.Text;
48
import javafx.stage.Window;
49
import org.fxmisc.richtext.StyleClassedTextArea;
50
import org.fxmisc.richtext.model.TwoDimensional.Position;
51
import org.fxmisc.undo.UndoManager;
52
import org.mozilla.universalchardet.UniversalDetector;
53
54
import java.io.File;
55
import java.nio.charset.Charset;
56
import java.nio.file.Files;
57
import java.nio.file.Path;
58
59
import static com.scrivenvar.Messages.get;
60
import static java.nio.charset.StandardCharsets.UTF_8;
61
import static java.util.Locale.ENGLISH;
62
import static org.fxmisc.richtext.model.TwoDimensional.Bias.Forward;
63
64
/**
65
 * Editor for a single file.
66
 *
67
 * @author Karl Tauber and White Magic Software, Ltd.
68
 */
69
public final class FileEditorTab extends Tab {
70
71
  private final Notifier mNotifier = Services.load( Notifier.class );
72
  private final EditorPane mEditorPane = new MarkdownEditorPane();
73
74
  private final ReadOnlyBooleanWrapper mModified = new ReadOnlyBooleanWrapper();
75
  private final BooleanProperty canUndo = new SimpleBooleanProperty();
76
  private final BooleanProperty canRedo = new SimpleBooleanProperty();
77
78
  /**
79
   * Character encoding used by the file (or default encoding if none found).
80
   */
81
  private Charset mEncoding = UTF_8;
82
83
  /**
84
   * File to load into the editor.
85
   */
86
  private Path mPath;
87
88
  public FileEditorTab( final Path path ) {
89
    setPath( path );
90
91
    mModified.addListener( ( observable, oldPath, newPath ) -> updateTab() );
92
93
    setOnSelectionChanged( e -> {
94
      if( isSelected() ) {
95
        Platform.runLater( this::activated );
96
      }
97
    } );
98
  }
99
100
  private void updateTab() {
101
    setText( getTabTitle() );
102
    setGraphic( getModifiedMark() );
103
    setTooltip( getTabTooltip() );
104
  }
105
106
  /**
107
   * Returns the base filename (without the directory names).
108
   *
109
   * @return The untitled text if the path hasn't been set.
110
   */
111
  private String getTabTitle() {
112
    return getPath().getFileName().toString();
113
  }
114
115
  /**
116
   * Returns the full filename represented by the path.
117
   *
118
   * @return The untitled text if the path hasn't been set.
119
   */
120
  private Tooltip getTabTooltip() {
121
    final Path filePath = getPath();
122
    return new Tooltip( filePath == null ? "" : filePath.toString() );
123
  }
124
125
  /**
126
   * Returns a marker to indicate whether the file has been modified.
127
   *
128
   * @return "*" when the file has changed; otherwise null.
129
   */
130
  private Text getModifiedMark() {
131
    return isModified() ? new Text( "*" ) : null;
132
  }
133
134
  /**
135
   * Called when the user switches tab.
136
   */
137
  private void activated() {
138
    // Tab is closed or no longer active.
139
    if( getTabPane() == null || !isSelected() ) {
140
      return;
141
    }
142
143
    // Switch to the tab without loading if the contents are already in memory.
144
    if( getContent() != null ) {
145
      getEditorPane().requestFocus();
146
      return;
147
    }
148
149
    // Load the text and update the preview before the undo manager.
150
    load();
151
152
    // Track undo requests -- can only be called *after* load.
153
    initUndoManager();
154
    initLayout();
155
    initFocus();
156
  }
157
158
  private void initLayout() {
159
    setContent( getScrollPane() );
160
  }
161
162
  private Node getScrollPane() {
163
    return getEditorPane().getScrollPane();
164
  }
165
166
  private void initFocus() {
167
    getEditorPane().requestFocus();
168
  }
169
170
  private void initUndoManager() {
171
    final UndoManager<?> undoManager = getUndoManager();
172
    undoManager.forgetHistory();
173
174
    // Bind the editor undo manager to the properties.
175
    mModified.bind( Bindings.not( undoManager.atMarkedPositionProperty() ) );
176
    canUndo.bind( undoManager.undoAvailableProperty() );
177
    canRedo.bind( undoManager.redoAvailableProperty() );
178
  }
179
180
  /**
181
   * Searches from the caret position forward for the given string.
182
   *
183
   * @param needle The text string to match.
184
   */
185
  public void searchNext( final String needle ) {
186
    final String haystack = getEditorText();
187
    int index = haystack.indexOf( needle, getCaretPosition() );
188
189
    // Wrap around.
190
    if( index == -1 ) {
191
      index = haystack.indexOf( needle );
192
    }
193
194
    if( index >= 0 ) {
195
      setCaretPosition( index );
196
      getEditor().selectRange( index, index + needle.length() );
197
    }
198
  }
199
200
  /**
201
   * Returns the index into the text where the caret blinks happily away.
202
   *
203
   * @return A number from 0 to the editor's document text length.
204
   */
205
  public int getCaretPosition() {
206
    return getEditor().getCaretPosition();
207
  }
208
209
  /**
210
   * Moves the caret to a given offset.
211
   *
212
   * @param offset The new caret offset.
213
   */
214
  private void setCaretPosition( final int offset ) {
215
    getEditor().moveTo( offset );
216
    getEditor().requestFollowCaret();
217
  }
218
219
  /**
220
   * Returns the caret's current row and column position.
221
   *
222
   * @return The caret's offset into the document.
223
   */
224
  public Position getCaretOffset() {
225
    return getEditor().offsetToPosition( getCaretPosition(), Forward );
226
  }
227
228
  /**
229
   * Allows observers to synchronize caret position changes.
230
   *
231
   * @return An observable caret property value.
232
   */
233
  public final ObservableValue<Integer> caretPositionProperty() {
234
    return getEditor().caretPositionProperty();
235
  }
236
237
  /**
238
   * Returns the text area associated with this tab.
239
   *
240
   * @return A text editor.
241
   */
242
  private StyleClassedTextArea getEditor() {
243
    return getEditorPane().getEditor();
244
  }
245
246
  /**
247
   * Returns true if the given path exactly matches this tab's path.
248
   *
249
   * @param check The path to compare against.
250
   * @return true The paths are the same.
251
   */
252
  public boolean isPath( final Path check ) {
253
    final Path filePath = getPath();
254
255
    return filePath != null && filePath.equals( check );
256
  }
257
258
  /**
259
   * Reads the entire file contents from the path associated with this tab.
260
   */
261
  private void load() {
262
    final Path path = getPath();
263
    final File file = path.toFile();
264
265
    try {
266
      if( file.exists() ) {
267
        if( file.canWrite() && file.canRead() ) {
268
          final EditorPane pane = getEditorPane();
269
          pane.setText( asString( Files.readAllBytes( path ) ) );
270
          pane.scrollToTop();
271
        }
272
        else {
273
          final String msg = get(
274
              "FileEditor.loadFailed.message",
275
              file.toString(),
276
              get( "FileEditor.loadFailed.reason.permissions" )
277
          );
278
          getNotifier().notify( msg );
279
        }
280
      }
281
    } catch( final Exception ex ) {
282
      getNotifier().notify( ex );
283
    }
284
  }
285
286
  /**
287
   * Saves the entire file contents from the path associated with this tab.
288
   *
289
   * @return true The file has been saved.
290
   */
291
  public boolean save() {
292
    try {
293
      final EditorPane editor = getEditorPane();
294
      Files.write( getPath(), asBytes( editor.getText() ) );
295
      editor.getUndoManager().mark();
296
      return true;
297
    } catch( final Exception ex ) {
298
      return alert(
299
          "FileEditor.saveFailed.title",
300
          "FileEditor.saveFailed.message",
301
          ex
302
      );
303
    }
304
  }
305
306
  /**
307
   * Creates an alert dialog and waits for it to close.
308
   *
309
   * @param titleKey   Resource bundle key for the alert dialog title.
310
   * @param messageKey Resource bundle key for the alert dialog message.
311
   * @param e          The unexpected happening.
312
   * @return false
313
   */
314
  @SuppressWarnings("SameParameterValue")
315
  private boolean alert(
316
      final String titleKey, final String messageKey, final Exception e ) {
317
    final Notifier service = getNotifier();
318
    final Path filePath = getPath();
319
320
    final Notification message = service.createNotification(
321
        get( titleKey ),
322
        get( messageKey ),
323
        filePath == null ? "" : filePath,
324
        e.getMessage()
325
    );
326
327
    try {
328
      service.createError( getWindow(), message ).showAndWait();
329
    } catch( final Exception ex ) {
330
      getNotifier().notify( ex );
331
    }
332
333
    return false;
334
  }
335
336
  private Window getWindow() {
337
    final Scene scene = getEditorPane().getScene();
338
339
    if( scene == null ) {
340
      throw new UnsupportedOperationException( "No scene window available" );
341
    }
342
343
    return scene.getWindow();
344
  }
345
346
  /**
347
   * Returns a best guess at the file encoding. If the encoding could not be
348
   * detected, this will return the default charset for the JVM.
349
   *
350
   * @param bytes The bytes to perform character encoding detection.
351
   * @return The character encoding.
352
   */
353
  private Charset detectEncoding( final byte[] bytes ) {
354
    final var detector = new UniversalDetector( null );
355
    detector.handleData( bytes, 0, bytes.length );
356
    detector.dataEnd();
357
358
    final String charset = detector.getDetectedCharset();
359
360
    return charset == null
361
        ? Charset.defaultCharset()
362
        : Charset.forName( charset.toUpperCase( ENGLISH ) );
363
  }
364
365
  /**
366
   * Converts the given string to an array of bytes using the encoding that was
367
   * originally detected (if any) and associated with this file.
368
   *
369
   * @param text The text to convert into the original file encoding.
370
   * @return A series of bytes ready for writing to a file.
371
   */
372
  private byte[] asBytes( final String text ) {
373
    return text.getBytes( getEncoding() );
374
  }
375
376
  /**
377
   * Converts the given bytes into a Java String. This will call setEncoding
378
   * with the encoding detected by the CharsetDetector.
379
   *
380
   * @param text The text of unknown character encoding.
381
   * @return The text, in its auto-detected encoding, as a String.
382
   */
383
  private String asString( final byte[] text ) {
384
    setEncoding( detectEncoding( text ) );
385
    return new String( text, getEncoding() );
386
  }
387
388
  /**
389
   * Returns the path to the file being edited in this tab.
390
   *
391
   * @return A non-null instance.
392
   */
393
  public Path getPath() {
394
    return mPath;
395
  }
396
397
  /**
398
   * Sets the path to a file for editing and then updates the tab with the
399
   * file contents.
400
   *
401
   * @param path A non-null instance.
402
   */
403
  public void setPath( final Path path ) {
404
    assert path != null;
405
406
    mPath = path;
407
408
    updateTab();
409
  }
410
411
  public boolean isModified() {
412
    return mModified.get();
413
  }
414
415
  ReadOnlyBooleanProperty modifiedProperty() {
416
    return mModified.getReadOnlyProperty();
417
  }
418
419
  BooleanProperty canUndoProperty() {
420
    return this.canUndo;
421
  }
422
423
  BooleanProperty canRedoProperty() {
424
    return this.canRedo;
425
  }
426
427
  private UndoManager<?> getUndoManager() {
428
    return getEditorPane().getUndoManager();
429
  }
430
431
  /**
432
   * Forwards to the editor pane's listeners for text change events.
433
   *
434
   * @param listener The listener to notify when the text changes.
435
   */
436
  public void addTextChangeListener( final ChangeListener<String> listener ) {
437
    getEditorPane().addTextChangeListener( listener );
438
  }
439
440
  /**
441
   * Forwards to the editor pane's listeners for caret paragraph change events.
442
   *
443
   * @param listener The listener to notify when the caret changes paragraphs.
444
   */
445
  public void addCaretParagraphListener(
446
      final ChangeListener<Integer> listener ) {
447
    getEditorPane().addCaretParagraphListener( listener );
448
  }
449
450
  public <T extends Event> void addEventFilter(
451
      final EventType<T> eventType,
452
      final EventHandler<? super T> eventFilter ) {
453
    getEditorPane().getEditor().addEventFilter( eventType, eventFilter );
454
  }
455
456
  /**
457
   * Forwards the request to the editor pane.
458
   *
459
   * @return The text to process.
460
   */
461
  public String getEditorText() {
462
    return getEditorPane().getText();
463
  }
464
465
  /**
466
   * Returns the editor pane, or creates one if it doesn't yet exist.
467
   *
468
   * @return The editor pane, never null.
469
   */
470
  public EditorPane getEditorPane() {
471
    return mEditorPane;
472
  }
473
474
  /**
475
   * Returns the encoding for the file, defaulting to UTF-8 if it hasn't been
476
   * determined.
477
   *
478
   * @return The file encoding or UTF-8 if unknown.
479
   */
480
  private Charset getEncoding() {
481
    return mEncoding;
482
  }
483
484
  private void setEncoding( final Charset encoding ) {
485
    assert encoding != null;
486
487
    mEncoding = encoding;
488
  }
489
490
  private Notifier getNotifier() {
491
    return mNotifier;
492
  }
493
494
  /**
495
   * Returns the tab title, without any modified indicators.
496
   *
497
   * @return The tab title.
498
   */
499
  @Override
500
  public String toString() {
501
    return getTabTitle();
502
  }
503
}
1504
A src/main/java/com/scrivenvar/FileEditorTabPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.predicates.files.FileTypePredicate;
31
import com.scrivenvar.service.Options;
32
import com.scrivenvar.service.Settings;
33
import com.scrivenvar.service.events.Notification;
34
import com.scrivenvar.service.events.Notifier;
35
import com.scrivenvar.util.Utils;
36
import javafx.beans.property.ReadOnlyBooleanProperty;
37
import javafx.beans.property.ReadOnlyBooleanWrapper;
38
import javafx.beans.property.ReadOnlyObjectProperty;
39
import javafx.beans.property.ReadOnlyObjectWrapper;
40
import javafx.beans.value.ChangeListener;
41
import javafx.beans.value.ObservableValue;
42
import javafx.collections.ListChangeListener;
43
import javafx.collections.ObservableList;
44
import javafx.event.Event;
45
import javafx.scene.Node;
46
import javafx.scene.control.Alert;
47
import javafx.scene.control.ButtonType;
48
import javafx.scene.control.Tab;
49
import javafx.scene.control.TabPane;
50
import javafx.stage.FileChooser;
51
import javafx.stage.FileChooser.ExtensionFilter;
52
import javafx.stage.Window;
53
54
import java.io.File;
55
import java.nio.file.Path;
56
import java.util.ArrayList;
57
import java.util.List;
58
import java.util.Optional;
59
import java.util.concurrent.atomic.AtomicReference;
60
import java.util.function.Consumer;
61
import java.util.prefs.Preferences;
62
import java.util.stream.Collectors;
63
64
import static com.scrivenvar.Constants.GLOB_PREFIX_FILE;
65
import static com.scrivenvar.FileType.*;
66
import static com.scrivenvar.Messages.get;
67
import static com.scrivenvar.service.events.Notifier.YES;
68
69
/**
70
 * Tab pane for file editors.
71
 *
72
 * @author Karl Tauber and White Magic Software, Ltd.
73
 */
74
public final class FileEditorTabPane extends TabPane {
75
76
  private final static String FILTER_EXTENSION_TITLES =
77
      "Dialog.file.choose.filter";
78
79
  private final static Options sOptions = Services.load( Options.class );
80
  private final static Settings sSettings = Services.load( Settings.class );
81
  private final static Notifier sNotifier = Services.load( Notifier.class );
82
83
  private final ReadOnlyObjectWrapper<Path> openDefinition =
84
      new ReadOnlyObjectWrapper<>();
85
  private final ReadOnlyObjectWrapper<FileEditorTab> mActiveFileEditor =
86
      new ReadOnlyObjectWrapper<>();
87
  private final ReadOnlyBooleanWrapper anyFileEditorModified =
88
      new ReadOnlyBooleanWrapper();
89
  private final Consumer<Double> mScrollEventObserver;
90
91
  /**
92
   * Constructs a new file editor tab pane.
93
   */
94
  public FileEditorTabPane( final Consumer<Double> scrollEventObserver ) {
95
    final ObservableList<Tab> tabs = getTabs();
96
97
    setFocusTraversable( false );
98
    setTabClosingPolicy( TabClosingPolicy.ALL_TABS );
99
100
    addTabSelectionListener(
101
        ( ObservableValue<? extends Tab> tabPane,
102
          final Tab oldTab, final Tab newTab ) -> {
103
104
          if( newTab != null ) {
105
            mActiveFileEditor.set( (FileEditorTab) newTab );
106
          }
107
        }
108
    );
109
110
    final ChangeListener<Boolean> modifiedListener = ( observable, oldValue,
111
                                                       newValue ) -> {
112
      for( final Tab tab : tabs ) {
113
        if( ((FileEditorTab) tab).isModified() ) {
114
          this.anyFileEditorModified.set( true );
115
          break;
116
        }
117
      }
118
    };
119
120
    tabs.addListener(
121
        (ListChangeListener<Tab>) change -> {
122
          while( change.next() ) {
123
            if( change.wasAdded() ) {
124
              change.getAddedSubList().forEach(
125
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
126
                                                  .addListener( modifiedListener ) );
127
            }
128
            else if( change.wasRemoved() ) {
129
              change.getRemoved().forEach(
130
                  ( tab ) -> ((FileEditorTab) tab).modifiedProperty()
131
                                                  .removeListener(
132
                                                      modifiedListener ) );
133
            }
134
          }
135
136
          // Changes in the tabs may also change anyFileEditorModified property
137
          // (e.g. closed modified file)
138
          modifiedListener.changed( null, null, null );
139
        }
140
    );
141
142
    mScrollEventObserver = scrollEventObserver;
143
  }
144
145
  /**
146
   * Allows observers to be notified when the current file editor tab changes.
147
   *
148
   * @param listener The listener to notify of tab change events.
149
   */
150
  public void addTabSelectionListener( final ChangeListener<Tab> listener ) {
151
    // Observe the tab so that when a new tab is opened or selected,
152
    // a notification is kicked off.
153
    getSelectionModel().selectedItemProperty().addListener( listener );
154
  }
155
156
  /**
157
   * Returns the tab that has keyboard focus.
158
   *
159
   * @return A non-null instance.
160
   */
161
  public FileEditorTab getActiveFileEditor() {
162
    return mActiveFileEditor.get();
163
  }
164
165
  /**
166
   * Returns the property corresponding to the tab that has focus.
167
   *
168
   * @return A non-null instance.
169
   */
170
  public ReadOnlyObjectProperty<FileEditorTab> activeFileEditorProperty() {
171
    return mActiveFileEditor.getReadOnlyProperty();
172
  }
173
174
  /**
175
   * Property that can answer whether the text has been modified.
176
   *
177
   * @return A non-null instance, true meaning the content has not been saved.
178
   */
179
  ReadOnlyBooleanProperty anyFileEditorModifiedProperty() {
180
    return this.anyFileEditorModified.getReadOnlyProperty();
181
  }
182
183
  /**
184
   * Creates a new editor instance from the given path.
185
   *
186
   * @param path The file to open.
187
   * @return A non-null instance.
188
   */
189
  private FileEditorTab createFileEditor( final Path path ) {
190
    assert path != null;
191
192
    final FileEditorTab tab = new FileEditorTab( path );
193
194
    tab.getEditorPane().getScrollPane().estimatedScrollYProperty().addObserver(
195
        mScrollEventObserver
196
    );
197
198
    tab.setOnCloseRequest( e -> {
199
      if( !canCloseEditor( tab ) ) {
200
        e.consume();
201
      }
202
      else if( isActiveFileEditor( tab ) ) {
203
        // Prevent prompting the user to save when there are no file editor
204
        // tabs open.
205
        mActiveFileEditor.set( null );
206
      }
207
    } );
208
209
    return tab;
210
  }
211
212
  private boolean isActiveFileEditor( final FileEditorTab tab ) {
213
    return getActiveFileEditor() == tab;
214
  }
215
216
  private Path getDefaultPath() {
217
    final String filename = getDefaultFilename();
218
    return (new File( filename )).toPath();
219
  }
220
221
  private String getDefaultFilename() {
222
    return getSettings().getSetting( "file.default", "untitled.md" );
223
  }
224
225
  /**
226
   * Called when the user selects New from the File menu.
227
   */
228
  void newEditor() {
229
    final Path defaultPath = getDefaultPath();
230
    final FileEditorTab tab = createFileEditor( defaultPath );
231
232
    getTabs().add( tab );
233
    getSelectionModel().select( tab );
234
  }
235
236
  void openFileDialog() {
237
    final String title = get( "Dialog.file.choose.open.title" );
238
    final FileChooser dialog = createFileChooser( title );
239
    final List<File> files = dialog.showOpenMultipleDialog( getWindow() );
240
241
    if( files != null ) {
242
      openFiles( files );
243
    }
244
  }
245
246
  /**
247
   * Opens the files into new editors, unless one of those files was a
248
   * definition file. The definition file is loaded into the definition pane,
249
   * but only the first one selected (multiple definition files will result in a
250
   * warning).
251
   *
252
   * @param files The list of non-definition files that the were requested to
253
   *              open.
254
   */
255
  private void openFiles( final List<File> files ) {
256
    final List<String> extensions =
257
        createExtensionFilter( DEFINITION ).getExtensions();
258
    final FileTypePredicate predicate =
259
        new FileTypePredicate( extensions );
260
261
    // The user might have opened multiple definitions files. These will
262
    // be discarded from the text editable files.
263
    final List<File> definitions
264
        = files.stream().filter( predicate ).collect( Collectors.toList() );
265
266
    // Create a modifiable list to remove any definition files that were
267
    // opened.
268
    final List<File> editors = new ArrayList<>( files );
269
270
    if( !editors.isEmpty() ) {
271
      saveLastDirectory( editors.get( 0 ) );
272
    }
273
274
    editors.removeAll( definitions );
275
276
    // Open editor-friendly files (e.g,. Markdown, XML) in new tabs.
277
    if( !editors.isEmpty() ) {
278
      openEditors( editors, 0 );
279
    }
280
281
    if( !definitions.isEmpty() ) {
282
      openDefinition( definitions.get( 0 ) );
283
    }
284
  }
285
286
  private void openEditors( final List<File> files, final int activeIndex ) {
287
    final int fileTally = files.size();
288
    final List<Tab> tabs = getTabs();
289
290
    // Close single unmodified "Untitled" tab.
291
    if( tabs.size() == 1 ) {
292
      final FileEditorTab fileEditor = (FileEditorTab) (tabs.get( 0 ));
293
294
      if( fileEditor.getPath() == null && !fileEditor.isModified() ) {
295
        closeEditor( fileEditor, false );
296
      }
297
    }
298
299
    for( int i = 0; i < fileTally; i++ ) {
300
      final Path path = files.get( i ).toPath();
301
302
      FileEditorTab fileEditorTab = findEditor( path );
303
304
      // Only open new files.
305
      if( fileEditorTab == null ) {
306
        fileEditorTab = createFileEditor( path );
307
        getTabs().add( fileEditorTab );
308
      }
309
310
      // Select the first file in the list.
311
      if( i == activeIndex ) {
312
        getSelectionModel().select( fileEditorTab );
313
      }
314
    }
315
  }
316
317
  /**
318
   * Returns a property that changes when a new definition file is opened.
319
   *
320
   * @return The path to a definition file that was opened.
321
   */
322
  public ReadOnlyObjectProperty<Path> onOpenDefinitionFileProperty() {
323
    return getOnOpenDefinitionFile().getReadOnlyProperty();
324
  }
325
326
  private ReadOnlyObjectWrapper<Path> getOnOpenDefinitionFile() {
327
    return this.openDefinition;
328
  }
329
330
  /**
331
   * Called when the user has opened a definition file (using the file open
332
   * dialog box). This will replace the current set of definitions for the
333
   * active tab.
334
   *
335
   * @param definition The file to open.
336
   */
337
  private void openDefinition( final File definition ) {
338
    // TODO: Prevent reading this file twice when a new text document is opened.
339
    // (might be a matter of checking the value first).
340
    getOnOpenDefinitionFile().set( definition.toPath() );
341
  }
342
343
  /**
344
   * Called when the contents of the editor are to be saved.
345
   *
346
   * @param tab The tab containing content to save.
347
   * @return true The contents were saved (or needn't be saved).
348
   */
349
  public boolean saveEditor( final FileEditorTab tab ) {
350
    if( tab == null || !tab.isModified() ) {
351
      return true;
352
    }
353
354
    return tab.getPath() == null ? saveEditorAs( tab ) : tab.save();
355
  }
356
357
  /**
358
   * Opens the Save As dialog for the user to save the content under a new
359
   * path.
360
   *
361
   * @param tab The tab with contents to save.
362
   * @return true The contents were saved, or the tab was null.
363
   */
364
  public boolean saveEditorAs( final FileEditorTab tab ) {
365
    if( tab == null ) {
366
      return true;
367
    }
368
369
    getSelectionModel().select( tab );
370
371
    final FileChooser fileChooser = createFileChooser( get(
372
        "Dialog.file.choose.save.title" ) );
373
    final File file = fileChooser.showSaveDialog( getWindow() );
374
    if( file == null ) {
375
      return false;
376
    }
377
378
    saveLastDirectory( file );
379
    tab.setPath( file.toPath() );
380
381
    return tab.save();
382
  }
383
384
  void saveAllEditors() {
385
    for( final FileEditorTab fileEditor : getAllEditors() ) {
386
      saveEditor( fileEditor );
387
    }
388
  }
389
390
  /**
391
   * Answers whether the file has had modifications. '
392
   *
393
   * @param tab THe tab to check for modifications.
394
   * @return false The file is unmodified.
395
   */
396
  @SuppressWarnings("BooleanMethodIsAlwaysInverted")
397
  boolean canCloseEditor( final FileEditorTab tab ) {
398
    final AtomicReference<Boolean> canClose = new AtomicReference<>();
399
    canClose.set( true );
400
401
    if( tab.isModified() ) {
402
      final Notification message = getNotifyService().createNotification(
403
          Messages.get( "Alert.file.close.title" ),
404
          Messages.get( "Alert.file.close.text" ),
405
          tab.getText()
406
      );
407
408
      final Alert confirmSave = getNotifyService().createConfirmation(
409
          getWindow(), message );
410
411
      final Optional<ButtonType> buttonType = confirmSave.showAndWait();
412
413
      buttonType.ifPresent(
414
          save -> canClose.set(
415
              save == YES ? saveEditor( tab ) : save == ButtonType.NO
416
          )
417
      );
418
    }
419
420
    return canClose.get();
421
  }
422
423
  boolean closeEditor( final FileEditorTab tab, final boolean save ) {
424
    if( tab == null ) {
425
      return true;
426
    }
427
428
    if( save ) {
429
      Event event = new Event( tab, tab, Tab.TAB_CLOSE_REQUEST_EVENT );
430
      Event.fireEvent( tab, event );
431
432
      if( event.isConsumed() ) {
433
        return false;
434
      }
435
    }
436
437
    getTabs().remove( tab );
438
439
    if( tab.getOnClosed() != null ) {
440
      Event.fireEvent( tab, new Event( Tab.CLOSED_EVENT ) );
441
    }
442
443
    return true;
444
  }
445
446
  boolean closeAllEditors() {
447
    final FileEditorTab[] allEditors = getAllEditors();
448
    final FileEditorTab activeEditor = getActiveFileEditor();
449
450
    // try to save active tab first because in case the user decides to cancel,
451
    // then it stays active
452
    if( activeEditor != null && !canCloseEditor( activeEditor ) ) {
453
      return false;
454
    }
455
456
    // This should be called any time a tab changes.
457
    persistPreferences();
458
459
    // save modified tabs
460
    for( int i = 0; i < allEditors.length; i++ ) {
461
      final FileEditorTab fileEditor = allEditors[ i ];
462
463
      if( fileEditor == activeEditor ) {
464
        continue;
465
      }
466
467
      if( fileEditor.isModified() ) {
468
        // activate the modified tab to make its modified content visible to
469
        // the user
470
        getSelectionModel().select( i );
471
472
        if( !canCloseEditor( fileEditor ) ) {
473
          return false;
474
        }
475
      }
476
    }
477
478
    // Close all tabs.
479
    for( final FileEditorTab fileEditor : allEditors ) {
480
      if( !closeEditor( fileEditor, false ) ) {
481
        return false;
482
      }
483
    }
484
485
    return getTabs().isEmpty();
486
  }
487
488
  private FileEditorTab[] getAllEditors() {
489
    final ObservableList<Tab> tabs = getTabs();
490
    final int length = tabs.size();
491
    final FileEditorTab[] allEditors = new FileEditorTab[ length ];
492
493
    for( int i = 0; i < length; i++ ) {
494
      allEditors[ i ] = (FileEditorTab) tabs.get( i );
495
    }
496
497
    return allEditors;
498
  }
499
500
  /**
501
   * Returns the file editor tab that has the given path.
502
   *
503
   * @return null No file editor tab for the given path was found.
504
   */
505
  private FileEditorTab findEditor( final Path path ) {
506
    for( final Tab tab : getTabs() ) {
507
      final FileEditorTab fileEditor = (FileEditorTab) tab;
508
509
      if( fileEditor.isPath( path ) ) {
510
        return fileEditor;
511
      }
512
    }
513
514
    return null;
515
  }
516
517
  private FileChooser createFileChooser( String title ) {
518
    final FileChooser fileChooser = new FileChooser();
519
520
    fileChooser.setTitle( title );
521
    fileChooser.getExtensionFilters().addAll(
522
        createExtensionFilters() );
523
524
    final String lastDirectory = getPreferences().get( "lastDirectory", null );
525
    File file = new File( (lastDirectory != null) ? lastDirectory : "." );
526
527
    if( !file.isDirectory() ) {
528
      file = new File( "." );
529
    }
530
531
    fileChooser.setInitialDirectory( file );
532
    return fileChooser;
533
  }
534
535
  private List<ExtensionFilter> createExtensionFilters() {
536
    final List<ExtensionFilter> list = new ArrayList<>();
537
538
    // TODO: Return a list of all properties that match the filter prefix.
539
    // This will allow dynamic filters to be added and removed just by
540
    // updating the properties file.
541
    list.add( createExtensionFilter( ALL ) );
542
    list.add( createExtensionFilter( SOURCE ) );
543
    list.add( createExtensionFilter( DEFINITION ) );
544
    list.add( createExtensionFilter( XML ) );
545
    return list;
546
  }
547
548
  /**
549
   * Returns a filter for file name extensions recognized by the application
550
   * that can be opened by the user.
551
   *
552
   * @param filetype Used to find the globbing pattern for extensions.
553
   * @return A filename filter suitable for use by a FileDialog instance.
554
   */
555
  private ExtensionFilter createExtensionFilter( final FileType filetype ) {
556
    final String tKey = String.format( "%s.title.%s",
557
                                       FILTER_EXTENSION_TITLES,
558
                                       filetype );
559
    final String eKey = String.format( "%s.%s", GLOB_PREFIX_FILE, filetype );
560
561
    return new ExtensionFilter( Messages.get( tKey ), getExtensions( eKey ) );
562
  }
563
564
  private List<String> getExtensions( final String key ) {
565
    return getSettings().getStringSettingList( key );
566
  }
567
568
  private void saveLastDirectory( final File file ) {
569
    getPreferences().put( "lastDirectory", file.getParent() );
570
  }
571
572
  public void restorePreferences() {
573
    int activeIndex = 0;
574
575
    final Preferences preferences = getPreferences();
576
    final String[] fileNames = Utils.getPrefsStrings( preferences, "file" );
577
    final String activeFileName = preferences.get( "activeFile", null );
578
579
    final List<File> files = new ArrayList<>( fileNames.length );
580
581
    for( final String fileName : fileNames ) {
582
      final File file = new File( fileName );
583
584
      if( file.exists() ) {
585
        files.add( file );
586
587
        if( fileName.equals( activeFileName ) ) {
588
          activeIndex = files.size() - 1;
589
        }
590
      }
591
    }
592
593
    if( files.isEmpty() ) {
594
      newEditor();
595
    }
596
    else {
597
      openEditors( files, activeIndex );
598
    }
599
  }
600
601
  public void persistPreferences() {
602
    final ObservableList<Tab> allEditors = getTabs();
603
    final List<String> fileNames = new ArrayList<>( allEditors.size() );
604
605
    for( final Tab tab : allEditors ) {
606
      final FileEditorTab fileEditor = (FileEditorTab) tab;
607
      final Path filePath = fileEditor.getPath();
608
609
      if( filePath != null ) {
610
        fileNames.add( filePath.toString() );
611
      }
612
    }
613
614
    final Preferences preferences = getPreferences();
615
    Utils.putPrefsStrings( preferences,
616
                           "file",
617
                           fileNames.toArray( new String[ 0 ] ) );
618
619
    final FileEditorTab activeEditor = getActiveFileEditor();
620
    final Path filePath = activeEditor == null ? null : activeEditor.getPath();
621
622
    if( filePath == null ) {
623
      preferences.remove( "activeFile" );
624
    }
625
    else {
626
      preferences.put( "activeFile", filePath.toString() );
627
    }
628
  }
629
630
  private Notifier getNotifyService() {
631
    return sNotifier;
632
  }
633
634
  private Settings getSettings() {
635
    return sSettings;
636
  }
637
638
  protected Options getOptions() {
639
    return sOptions;
640
  }
641
642
  private Window getWindow() {
643
    return getScene().getWindow();
644
  }
645
646
  private Preferences getPreferences() {
647
    return getOptions().getState();
648
  }
649
650
  Node getNode() {
651
    return this;
652
  }
653
}
1654
A src/main/java/com/scrivenvar/FileType.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
/**
31
 * Represents different file type classifications. These are high-level mappings
32
 * that correspond to the list of glob patterns found within
33
 * settings.properties.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public enum FileType {
38
39
  ALL( "all" ),
40
  RMARKDOWN( "rmarkdown" ),
41
  RXML( "rxml" ),
42
  SOURCE( "source" ),
43
  DEFINITION( "definition" ),
44
  XML( "xml" ),
45
  CSV( "csv" ),
46
  JSON( "json" ),
47
  TOML( "toml" ),
48
  YAML( "yaml" ),
49
  PROPERTIES( "properties" );
50
51
  private final String mType;
52
53
  /**
54
   * Default constructor for enumerated file type.
55
   *
56
   * @param type Human-readable name for the file type.
57
   */
58
  FileType( final String type ) {
59
    mType = type;
60
  }
61
62
  /**
63
   * Returns the file type that corresponds to the given string.
64
   *
65
   * @param type The string to compare against this enumeration of file types.
66
   *
67
   * @return The corresponding File Type for the given string.
68
   *
69
   * @throws IllegalArgumentException Type not found.
70
   */
71
  public static FileType from( final String type ) {
72
    for( final FileType fileType : FileType.values() ) {
73
      if( fileType.isType( type ) ) {
74
        return fileType;
75
      }
76
    }
77
78
    throw new IllegalArgumentException( type );
79
  }
80
81
  /**
82
   * Answers whether this file type matches the given string, case insensitive
83
   * comparison.
84
   *
85
   * @param type Presumably a file name extension to check against.
86
   *
87
   * @return true The given extension corresponds to this enumerated type.
88
   */
89
  public boolean isType( final String type ) {
90
    return getType().equalsIgnoreCase( type );
91
  }
92
93
  /**
94
   * Returns the human-readable name for the file type.
95
   *
96
   * @return A non-null instance.
97
   */
98
  private String getType() {
99
    return mType;
100
  }
101
102
  /**
103
   * Returns the lowercase version of the file name extension.
104
   *
105
   * @return The file name, in lower case.
106
   */
107
  @Override
108
  public String toString() {
109
    return getType();
110
  }
111
}
1112
A src/main/java/com/scrivenvar/Launcher.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import java.io.IOException;
31
import java.io.InputStream;
32
import java.util.Calendar;
33
import java.util.Properties;
34
35
import static java.lang.String.format;
36
37
/**
38
 * Launches the application using the {@link Main} class.
39
 *
40
 * <p>
41
 * This is required until modules are implemented, which may never happen
42
 * because the application should be ported away from Java and JavaFX.
43
 * </p>
44
 */
45
public class Launcher {
46
  /**
47
   * Delegates to the application entry point.
48
   *
49
   * @param args Command-line arguments.
50
   */
51
  public static void main( final String[] args ) throws IOException {
52
    // Shhh.
53
    System.err.close();
54
55
    showAppInfo();
56
    Main.main( args );
57
  }
58
59
  @SuppressWarnings("RedundantStringFormatCall")
60
  private static void showAppInfo() throws IOException {
61
    out( format( "%s version %s", getTitle(), getVersion() ) );
62
    out( format( "Copyright %s by White Magic Software, Ltd.", getYear() ) );
63
    out( format( "Portions copyright 2020 Karl Tauber." ) );
64
  }
65
66
  private static void out( final String s ) {
67
    System.out.println( s );
68
  }
69
70
  private static String getTitle() throws IOException {
71
    final Properties properties = loadProperties( "messages.properties" );
72
    return properties.getProperty( "Main.title" );
73
  }
74
75
  private static String getVersion() throws IOException {
76
    final Properties properties = loadProperties( "app.properties" );
77
    return properties.getProperty( "application.version" );
78
  }
79
80
  private static String getYear() {
81
    return Integer.toString( Calendar.getInstance().get( Calendar.YEAR ) );
82
  }
83
84
  @SuppressWarnings("SameParameterValue")
85
  private static Properties loadProperties( final String resource )
86
      throws IOException {
87
    final Properties properties = new Properties();
88
    properties.load( getResourceAsStream( getResourceName( resource ) ) );
89
    return properties;
90
  }
91
92
  private static String getResourceName( final String resource ) {
93
    return format( "%s/%s", getPackagePath(), resource );
94
  }
95
96
  private static String getPackagePath() {
97
    return Launcher.class.getPackageName().replace( '.', '/' );
98
  }
99
100
  private static InputStream getResourceAsStream( final String resource ) {
101
    return Launcher.class.getClassLoader().getResourceAsStream( resource );
102
  }
103
}
1104
A src/main/java/com/scrivenvar/Main.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.preferences.FilePreferencesFactory;
31
import com.scrivenvar.service.Options;
32
import com.scrivenvar.service.Snitch;
33
import com.scrivenvar.service.events.Notifier;
34
import com.scrivenvar.util.StageState;
35
import javafx.application.Application;
36
import javafx.scene.Scene;
37
import javafx.scene.image.Image;
38
import javafx.stage.Stage;
39
40
import java.util.logging.LogManager;
41
42
import static com.scrivenvar.Constants.*;
43
import static com.scrivenvar.Messages.get;
44
45
/**
46
 * Application entry point. The application allows users to edit Markdown
47
 * files and see a real-time preview of the edits.
48
 *
49
 * @author Karl Tauber and White Magic Software, Ltd.
50
 */
51
public final class Main extends Application {
52
53
  // Suppress logging errors to standard output.
54
  static {
55
    LogManager.getLogManager().reset();
56
  }
57
58
  private static Application sApplication;
59
60
  private final Options mOptions = Services.load( Options.class );
61
  private final Notifier mNotifier = Services.load( Notifier.class );
62
  private final Snitch mSnitch = Services.load( Snitch.class );
63
  private final Thread mSnitchThread = new Thread( getSnitch() );
64
  private final MainWindow mMainWindow = new MainWindow();
65
66
  @SuppressWarnings({"FieldCanBeLocal", "unused"})
67
  private StageState mStageState;
68
69
  /**
70
   * Application entry point.
71
   *
72
   * @param args Command-line arguments.
73
   */
74
  public static void main( final String[] args ) {
75
    initPreferences();
76
    launch( args );
77
  }
78
79
  /**
80
   * JavaFX entry point.
81
   *
82
   * @param stage The primary application stage.
83
   */
84
  @Override
85
  public void start( final Stage stage ) {
86
    initApplication();
87
    initNotifyService();
88
    initState( stage );
89
    initStage( stage );
90
    initSnitch();
91
92
    stage.show();
93
  }
94
95
  /**
96
   * Sets the factory used for reading user preferences.
97
   */
98
  private static void initPreferences() {
99
    System.setProperty(
100
        "java.util.prefs.PreferencesFactory",
101
        FilePreferencesFactory.class.getName()
102
    );
103
  }
104
105
  public static void showDocument( final String uri ) {
106
    getApplication().getHostServices().showDocument( uri );
107
  }
108
109
  private void initApplication() {
110
    sApplication = this;
111
  }
112
113
  /**
114
   * Constructs the notify service and appends the main window to the list of
115
   * notification observers.
116
   */
117
  private void initNotifyService() {
118
    mNotifier.addObserver( getMainWindow() );
119
  }
120
121
  private void initState( final Stage stage ) {
122
    mStageState = new StageState( stage, getOptions().getState() );
123
  }
124
125
  private void initStage( final Stage stage ) {
126
    stage.getIcons().addAll(
127
        createImage( FILE_LOGO_16 ),
128
        createImage( FILE_LOGO_32 ),
129
        createImage( FILE_LOGO_128 ),
130
        createImage( FILE_LOGO_256 ),
131
        createImage( FILE_LOGO_512 ) );
132
    stage.setTitle( getApplicationTitle() );
133
    stage.setScene( getScene() );
134
  }
135
136
  /**
137
   * Watch for file system changes.
138
   */
139
  private void initSnitch() {
140
    getSnitchThread().start();
141
  }
142
143
  /**
144
   * Stops the snitch service, if its running.
145
   *
146
   * @throws InterruptedException Couldn't stop the snitch thread.
147
   */
148
  @Override
149
  public void stop() throws InterruptedException {
150
    getSnitch().stop();
151
152
    final Thread thread = getSnitchThread();
153
    thread.interrupt();
154
    thread.join();
155
  }
156
157
  private synchronized Snitch getSnitch() {
158
    return mSnitch;
159
  }
160
161
  private Thread getSnitchThread() {
162
    return mSnitchThread;
163
  }
164
165
  private synchronized Options getOptions() {
166
    return mOptions;
167
  }
168
169
  private Scene getScene() {
170
    return getMainWindow().getScene();
171
  }
172
173
  private MainWindow getMainWindow() {
174
    return mMainWindow;
175
  }
176
177
  private static Application getApplication() {
178
    return sApplication;
179
  }
180
181
  private String getApplicationTitle() {
182
    return get( "Main.title" );
183
  }
184
185
  private Image createImage( final String filename ) {
186
    return new Image( filename );
187
  }
188
}
1189
A src/main/java/com/scrivenvar/MainWindow.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import com.scrivenvar.definition.DefinitionFactory;
31
import com.scrivenvar.definition.DefinitionPane;
32
import com.scrivenvar.definition.DefinitionSource;
33
import com.scrivenvar.definition.MapInterpolator;
34
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
35
import com.scrivenvar.editors.EditorPane;
36
import com.scrivenvar.editors.VariableNameInjector;
37
import com.scrivenvar.editors.markdown.MarkdownEditorPane;
38
import com.scrivenvar.preferences.UserPreferences;
39
import com.scrivenvar.preview.HTMLPreviewPane;
40
import com.scrivenvar.processors.Processor;
41
import com.scrivenvar.processors.ProcessorFactory;
42
import com.scrivenvar.service.Options;
43
import com.scrivenvar.service.Snitch;
44
import com.scrivenvar.service.events.Notifier;
45
import com.scrivenvar.util.Action;
46
import com.scrivenvar.util.ActionBuilder;
47
import com.scrivenvar.util.ActionUtils;
48
import javafx.application.Platform;
49
import javafx.beans.binding.Bindings;
50
import javafx.beans.binding.BooleanBinding;
51
import javafx.beans.property.BooleanProperty;
52
import javafx.beans.property.SimpleBooleanProperty;
53
import javafx.beans.value.ObservableBooleanValue;
54
import javafx.beans.value.ObservableValue;
55
import javafx.collections.ListChangeListener.Change;
56
import javafx.collections.ObservableList;
57
import javafx.event.Event;
58
import javafx.event.EventHandler;
59
import javafx.geometry.Pos;
60
import javafx.scene.Node;
61
import javafx.scene.Scene;
62
import javafx.scene.control.*;
63
import javafx.scene.control.Alert.AlertType;
64
import javafx.scene.image.Image;
65
import javafx.scene.image.ImageView;
66
import javafx.scene.input.KeyEvent;
67
import javafx.scene.layout.BorderPane;
68
import javafx.scene.layout.VBox;
69
import javafx.scene.text.Text;
70
import javafx.stage.Window;
71
import javafx.stage.WindowEvent;
72
import javafx.util.Duration;
73
import org.controlsfx.control.StatusBar;
74
import org.fxmisc.richtext.model.TwoDimensional.Position;
75
76
import java.io.File;
77
import java.nio.file.Path;
78
import java.util.HashMap;
79
import java.util.Map;
80
import java.util.Observable;
81
import java.util.Observer;
82
import java.util.concurrent.atomic.AtomicInteger;
83
import java.util.function.Consumer;
84
import java.util.function.Function;
85
import java.util.prefs.Preferences;
86
87
import static com.scrivenvar.Constants.*;
88
import static com.scrivenvar.Messages.get;
89
import static com.scrivenvar.util.StageState.*;
90
import static de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon.*;
91
import static javafx.event.Event.fireEvent;
92
import static javafx.scene.input.KeyCode.ENTER;
93
import static javafx.scene.input.KeyCode.TAB;
94
import static javafx.stage.WindowEvent.WINDOW_CLOSE_REQUEST;
95
96
/**
97
 * Main window containing a tab pane in the center for file editors.
98
 *
99
 * @author Karl Tauber and White Magic Software, Ltd.
100
 */
101
public class MainWindow implements Observer {
102
103
  /**
104
   * The {@code OPTIONS} variable must be declared before all other variables
105
   * to prevent subsequent initializations from failing due to missing user
106
   * preferences.
107
   */
108
  private final static Options OPTIONS = Services.load( Options.class );
109
  private final static Snitch SNITCH = Services.load( Snitch.class );
110
  private final static Notifier NOTIFIER = Services.load( Notifier.class );
111
112
  private final Scene mScene;
113
  private final StatusBar mStatusBar;
114
  private final Text mLineNumberText;
115
  private final TextField mFindTextField;
116
117
  private DefinitionSource mDefinitionSource = createDefaultDefinitionSource();
118
  private final DefinitionPane mDefinitionPane = new DefinitionPane();
119
  private final HTMLPreviewPane mPreviewPane = createHTMLPreviewPane();
120
  private FileEditorTabPane fileEditorPane;
121
122
  /**
123
   * Prevents re-instantiation of processing classes.
124
   */
125
  private final Map<FileEditorTab, Processor<String>> mProcessors =
126
      new HashMap<>();
127
128
  private final Map<String, String> mResolvedMap =
129
      new HashMap<>( DEFAULT_MAP_SIZE );
130
131
  /**
132
   * Listens on the definition pane for double-click events.
133
   */
134
  private VariableNameInjector variableNameInjector;
135
136
  /**
137
   * Called when the definition data is changed.
138
   */
139
  private final EventHandler<TreeItem.TreeModificationEvent<Event>>
140
      mTreeHandler = event -> {
141
    exportDefinitions( getDefinitionPath() );
142
    interpolateResolvedMap();
143
    refreshActiveTab();
144
  };
145
146
  /**
147
   * Called to inject the selected item when the user presses ENTER in the
148
   * definition pane.
149
   */
150
  private final EventHandler<? super KeyEvent> mDefinitionKeyHandler =
151
      event -> {
152
        if( event.getCode() == ENTER ) {
153
          getVariableNameInjector().injectSelectedItem();
154
        }
155
      };
156
157
  /**
158
   * Called to switch to the definition pane when the user presses TAB.
159
   */
160
  private final EventHandler<? super KeyEvent> mEditorKeyHandler =
161
      (EventHandler<KeyEvent>) event -> {
162
        if( event.getCode() == TAB ) {
163
          getDefinitionPane().requestFocus();
164
          event.consume();
165
        }
166
      };
167
168
  private final Object mMutex = new Object();
169
  private final AtomicInteger mScrollRatio = new AtomicInteger( 0 );
170
171
  /**
172
   * Called to synchronize the scrolling areas.
173
   */
174
  private final Consumer<Double> mScrollEventObserver = o -> {
175
    final var eScrollPane = getActiveEditor().getScrollPane();
176
    final int eScrollY =
177
        eScrollPane.estimatedScrollYProperty().getValue().intValue();
178
    final int eHeight = (int)
179
        (eScrollPane.totalHeightEstimateProperty().getValue().intValue()
180
            - eScrollPane.getHeight());
181
    final double eRatio = eHeight > 0
182
        ? Math.min( Math.max( eScrollY / (float) eHeight, 0 ), 1 ) : 0;
183
184
    final var pPreviewPane = getPreviewPane();
185
    final var pScrollBar = pPreviewPane.getVerticalScrollBar();
186
    final var pHeight = pScrollBar.getMaximum() - pScrollBar.getHeight();
187
    final var pScrollY = (int) (pHeight * eRatio);
188
    final var pScrollPane = pPreviewPane.getScrollPane();
189
190
    final int oldScrollY = mScrollRatio.getAndSet( pScrollY );
191
    final int delta = Math.abs( oldScrollY - pScrollY );
192
193
    if( delta > 33 ) {
194
      // Prevent concurrent modification exceptions when attempting to
195
      // set the vertical scroll bar position.
196
      synchronized( mMutex ) {
197
        Platform.runLater( () -> {
198
          pScrollBar.setValue( pScrollY );
199
          pScrollPane.repaint();
200
        } );
201
      }
202
    }
203
  };
204
205
  public MainWindow() {
206
    mStatusBar = createStatusBar();
207
    mLineNumberText = createLineNumberText();
208
    mFindTextField = createFindTextField();
209
    mScene = createScene();
210
211
    initLayout();
212
    initFindInput();
213
    initSnitch();
214
    initDefinitionListener();
215
    initTabAddedListener();
216
    initTabChangedListener();
217
    restorePreferences();
218
  }
219
220
  private void initLayout() {
221
    final Scene appScene = getScene();
222
223
    appScene.getStylesheets().add( STYLESHEET_SCENE );
224
225
    // TODO: Apply an XML syntax highlighting for XML files.
226
//    appScene.getStylesheets().add( STYLESHEET_XML );
227
    appScene.windowProperty().addListener(
228
        ( observable, oldWindow, newWindow ) ->
229
            newWindow.setOnCloseRequest(
230
                e -> {
231
                  if( !getFileEditorPane().closeAllEditors() ) {
232
                    e.consume();
233
                  }
234
                }
235
            )
236
    );
237
  }
238
239
  /**
240
   * Initialize the find input text field to listen on F3, ENTER, and ESCAPE key
241
   * presses.
242
   */
243
  private void initFindInput() {
244
    final TextField input = getFindTextField();
245
246
    input.setOnKeyPressed( ( KeyEvent event ) -> {
247
      switch( event.getCode() ) {
248
        case F3:
249
        case ENTER:
250
          editFindNext();
251
          break;
252
        case F:
253
          if( !event.isControlDown() ) {
254
            break;
255
          }
256
        case ESCAPE:
257
          getStatusBar().setGraphic( null );
258
          getActiveFileEditor().getEditorPane().requestFocus();
259
          break;
260
      }
261
    } );
262
263
    // Remove when the input field loses focus.
264
    input.focusedProperty().addListener(
265
        (
266
            final ObservableValue<? extends Boolean> focused,
267
            final Boolean oFocus,
268
            final Boolean nFocus ) -> {
269
          if( !nFocus ) {
270
            getStatusBar().setGraphic( null );
271
          }
272
        }
273
    );
274
  }
275
276
  /**
277
   * Watch for changes to external files. In particular, this awaits
278
   * modifications to any XSL files associated with XML files being edited. When
279
   * an XSL file is modified (external to the application), the snitch's ears
280
   * perk up and the file is reloaded. This keeps the XSL transformation up to
281
   * date with what's on the file system.
282
   */
283
  private void initSnitch() {
284
    SNITCH.addObserver( this );
285
  }
286
287
  /**
288
   * Listen for {@link FileEditorTabPane} to receive open definition file event.
289
   */
290
  private void initDefinitionListener() {
291
    getFileEditorPane().onOpenDefinitionFileProperty().addListener(
292
        ( final ObservableValue<? extends Path> file,
293
          final Path oldPath, final Path newPath ) -> {
294
          // Indirectly refresh the resolved map.
295
          resetProcessors();
296
297
          openDefinitions( newPath );
298
299
          // Will create new processors and therefore a new resolved map.
300
          refreshActiveTab();
301
        }
302
    );
303
  }
304
305
  /**
306
   * When tabs are added, hook the various change listeners onto the new tab so
307
   * that the preview pane refreshes as necessary.
308
   */
309
  private void initTabAddedListener() {
310
    final FileEditorTabPane editorPane = getFileEditorPane();
311
312
    // Make sure the text processor kicks off when new files are opened.
313
    final ObservableList<Tab> tabs = editorPane.getTabs();
314
315
    // Update the preview pane on tab changes.
316
    tabs.addListener(
317
        ( final Change<? extends Tab> change ) -> {
318
          while( change.next() ) {
319
            if( change.wasAdded() ) {
320
              // Multiple tabs can be added simultaneously.
321
              for( final Tab newTab : change.getAddedSubList() ) {
322
                final FileEditorTab tab = (FileEditorTab) newTab;
323
324
                initTextChangeListener( tab );
325
                initKeyboardEventListeners( tab );
326
//              initSyntaxListener( tab );
327
              }
328
            }
329
          }
330
        }
331
    );
332
  }
333
334
  /**
335
   * Listen for new tab selection events.
336
   */
337
  private void initTabChangedListener() {
338
    final FileEditorTabPane editorPane = getFileEditorPane();
339
340
    // Update the preview pane changing tabs.
341
    editorPane.addTabSelectionListener(
342
        ( ObservableValue<? extends Tab> tabPane,
343
          final Tab oldTab, final Tab newTab ) -> {
344
          updateVariableNameInjector();
345
346
          // If there was no old tab, then this is a first time load, which
347
          // can be ignored.
348
          if( oldTab != null ) {
349
            if( newTab == null ) {
350
              closeRemainingTab();
351
            }
352
            else {
353
              // Update the preview with the edited text.
354
              refreshSelectedTab( (FileEditorTab) newTab );
355
            }
356
          }
357
        }
358
    );
359
  }
360
361
  /**
362
   * Reloads the preferences from the previous session.
363
   */
364
  private void restorePreferences() {
365
    restoreDefinitionPane();
366
    getFileEditorPane().restorePreferences();
367
  }
368
369
  /**
370
   * Ensure that the keyboard events are received when a new tab is added
371
   * to the user interface.
372
   *
373
   * @param tab The tab that can trigger keyboard events, such as control+space.
374
   */
375
  private void initKeyboardEventListeners( final FileEditorTab tab ) {
376
    final VariableNameInjector vin = getVariableNameInjector();
377
    vin.initKeyboardEventListeners( tab );
378
379
    tab.addEventFilter( KeyEvent.KEY_PRESSED, mEditorKeyHandler );
380
  }
381
382
  private void initTextChangeListener( final FileEditorTab tab ) {
383
    tab.addTextChangeListener(
384
        ( ObservableValue<? extends String> editor,
385
          final String oldValue, final String newValue ) ->
386
            refreshSelectedTab( tab )
387
    );
388
  }
389
390
  private void updateVariableNameInjector() {
391
    getVariableNameInjector().setFileEditorTab( getActiveFileEditor() );
392
  }
393
394
  private void setVariableNameInjector( final VariableNameInjector injector ) {
395
    this.variableNameInjector = injector;
396
  }
397
398
  private synchronized VariableNameInjector getVariableNameInjector() {
399
    if( this.variableNameInjector == null ) {
400
      final VariableNameInjector vin = createVariableNameInjector();
401
      setVariableNameInjector( vin );
402
    }
403
404
    return this.variableNameInjector;
405
  }
406
407
  private VariableNameInjector createVariableNameInjector() {
408
    final FileEditorTab tab = getActiveFileEditor();
409
    final DefinitionPane pane = getDefinitionPane();
410
411
    return new VariableNameInjector( tab, pane );
412
  }
413
414
  /**
415
   * Called whenever the preview pane becomes out of sync with the file editor
416
   * tab. This can be called when the text changes, the caret paragraph changes,
417
   * or the file tab changes.
418
   *
419
   * @param tab The file editor tab that has been changed in some fashion.
420
   */
421
  private void refreshSelectedTab( final FileEditorTab tab ) {
422
    if( tab == null ) {
423
      return;
424
    }
425
426
    getPreviewPane().setPath( tab.getPath() );
427
428
    // TODO: https://github.com/DaveJarvis/scrivenvar/issues/29
429
    final Position p = tab.getCaretOffset();
430
    getLineNumberText().setText(
431
        get( STATUS_BAR_LINE,
432
             p.getMajor() + 1,
433
             p.getMinor() + 1,
434
             tab.getCaretPosition() + 1
435
        )
436
    );
437
438
    Processor<String> processor = getProcessors().get( tab );
439
440
    if( processor == null ) {
441
      processor = createProcessor( tab );
442
      getProcessors().put( tab, processor );
443
    }
444
445
    try {
446
      processor.processChain( tab.getEditorText() );
447
    } catch( final Exception ex ) {
448
      error( ex );
449
    }
450
  }
451
452
  private void refreshActiveTab() {
453
    refreshSelectedTab( getActiveFileEditor() );
454
  }
455
456
  /**
457
   * Called when a definition source is opened.
458
   *
459
   * @param path Path to the definition source that was opened.
460
   */
461
  private void openDefinitions( final Path path ) {
462
    try {
463
      final DefinitionSource ds = createDefinitionSource( path );
464
      setDefinitionSource( ds );
465
      getUserPreferences().definitionPathProperty().setValue( path.toFile() );
466
      getUserPreferences().save();
467
468
      final Tooltip tooltipPath = new Tooltip( path.toString() );
469
      tooltipPath.setShowDelay( Duration.millis( 200 ) );
470
471
      final DefinitionPane pane = getDefinitionPane();
472
      pane.update( ds );
473
      pane.addTreeChangeHandler( mTreeHandler );
474
      pane.addKeyEventHandler( mDefinitionKeyHandler );
475
      pane.filenameProperty().setValue( path.getFileName().toString() );
476
      pane.setTooltip( tooltipPath );
477
478
      interpolateResolvedMap();
479
    } catch( final Exception e ) {
480
      error( e );
481
    }
482
  }
483
484
  private void exportDefinitions( final Path path ) {
485
    try {
486
      final DefinitionPane pane = getDefinitionPane();
487
      final TreeItem<String> root = pane.getTreeView().getRoot();
488
      final TreeItem<String> problemChild = pane.isTreeWellFormed();
489
490
      if( problemChild == null ) {
491
        getDefinitionSource().getTreeAdapter().export( root, path );
492
        getNotifier().clear();
493
      }
494
      else {
495
        final String msg = get( "yaml.error.tree.form",
496
                                problemChild.getValue() );
497
        getNotifier().notify( msg );
498
      }
499
    } catch( final Exception e ) {
500
      error( e );
501
    }
502
  }
503
504
  private void interpolateResolvedMap() {
505
    final Map<String, String> treeMap = getDefinitionPane().toMap();
506
    final Map<String, String> map = new HashMap<>( treeMap );
507
    MapInterpolator.interpolate( map );
508
509
    getResolvedMap().clear();
510
    getResolvedMap().putAll( map );
511
  }
512
513
  private void restoreDefinitionPane() {
514
    openDefinitions( getDefinitionPath() );
515
  }
516
517
  /**
518
   * Called when the last open tab is closed to clear the preview pane.
519
   */
520
  private void closeRemainingTab() {
521
    getPreviewPane().clear();
522
  }
523
524
  /**
525
   * Called when an exception occurs that warrants the user's attention.
526
   *
527
   * @param e The exception with a message that the user should know about.
528
   */
529
  private void error( final Exception e ) {
530
    getNotifier().notify( e );
531
  }
532
533
  //---- File actions -------------------------------------------------------
534
535
  /**
536
   * Called when an observable instance has changed. This is called by both the
537
   * snitch service and the notify service. The snitch service can be called for
538
   * different file types, including definition sources.
539
   *
540
   * @param observable The observed instance.
541
   * @param value      The noteworthy item.
542
   */
543
  @Override
544
  public void update( final Observable observable, final Object value ) {
545
    if( value != null ) {
546
      if( observable instanceof Snitch && value instanceof Path ) {
547
        updateSelectedTab();
548
      }
549
      else if( observable instanceof Notifier && value instanceof String ) {
550
        updateStatusBar( (String) value );
551
      }
552
    }
553
  }
554
555
  /**
556
   * Updates the status bar to show the given message.
557
   *
558
   * @param s The message to show in the status bar.
559
   */
560
  private void updateStatusBar( final String s ) {
561
    Platform.runLater(
562
        () -> {
563
          final int index = s.indexOf( '\n' );
564
          final String message = s.substring(
565
              0, index > 0 ? index : s.length() );
566
567
          getStatusBar().setText( message );
568
        }
569
    );
570
  }
571
572
  /**
573
   * Called when a file has been modified.
574
   */
575
  private void updateSelectedTab() {
576
    Platform.runLater(
577
        () -> {
578
          // Brute-force XSLT file reload by re-instantiating all processors.
579
          resetProcessors();
580
          refreshActiveTab();
581
        }
582
    );
583
  }
584
585
  /**
586
   * After resetting the processors, they will refresh anew to be up-to-date
587
   * with the files (text and definition) currently loaded into the editor.
588
   */
589
  private void resetProcessors() {
590
    getProcessors().clear();
591
  }
592
593
  //---- File actions -------------------------------------------------------
594
595
  private void fileNew() {
596
    getFileEditorPane().newEditor();
597
  }
598
599
  private void fileOpen() {
600
    getFileEditorPane().openFileDialog();
601
  }
602
603
  private void fileClose() {
604
    getFileEditorPane().closeEditor( getActiveFileEditor(), true );
605
  }
606
607
  /**
608
   * TODO: Upon closing, first remove the tab change listeners. (There's no
609
   * need to re-render each tab when all are being closed.)
610
   */
611
  private void fileCloseAll() {
612
    getFileEditorPane().closeAllEditors();
613
  }
614
615
  private void fileSave() {
616
    getFileEditorPane().saveEditor( getActiveFileEditor() );
617
  }
618
619
  private void fileSaveAs() {
620
    final FileEditorTab editor = getActiveFileEditor();
621
    getFileEditorPane().saveEditorAs( editor );
622
    getProcessors().remove( editor );
623
624
    try {
625
      refreshSelectedTab( editor );
626
    } catch( final Exception ex ) {
627
      getNotifier().notify( ex );
628
    }
629
  }
630
631
  private void fileSaveAll() {
632
    getFileEditorPane().saveAllEditors();
633
  }
634
635
  private void fileExit() {
636
    final Window window = getWindow();
637
    fireEvent( window, new WindowEvent( window, WINDOW_CLOSE_REQUEST ) );
638
  }
639
640
  //---- Edit actions -------------------------------------------------------
641
642
  /**
643
   * Used to find text in the active file editor window.
644
   */
645
  private void editFind() {
646
    final TextField input = getFindTextField();
647
    getStatusBar().setGraphic( input );
648
    input.requestFocus();
649
  }
650
651
  public void editFindNext() {
652
    getActiveFileEditor().searchNext( getFindTextField().getText() );
653
  }
654
655
  public void editPreferences() {
656
    getUserPreferences().show();
657
  }
658
659
  //---- Insert actions -----------------------------------------------------
660
661
  /**
662
   * Delegates to the active editor to handle wrapping the current text
663
   * selection with leading and trailing strings.
664
   *
665
   * @param leading  The string to put before the selection.
666
   * @param trailing The string to put after the selection.
667
   */
668
  private void insertMarkdown(
669
      final String leading, final String trailing ) {
670
    getActiveEditor().surroundSelection( leading, trailing );
671
  }
672
673
  @SuppressWarnings("SameParameterValue")
674
  private void insertMarkdown(
675
      final String leading, final String trailing, final String hint ) {
676
    getActiveEditor().surroundSelection( leading, trailing, hint );
677
  }
678
679
  //---- Help actions -------------------------------------------------------
680
681
  private void helpAbout() {
682
    final Alert alert = new Alert( AlertType.INFORMATION );
683
    alert.setTitle( get( "Dialog.about.title" ) );
684
    alert.setHeaderText( get( "Dialog.about.header" ) );
685
    alert.setContentText( get( "Dialog.about.content" ) );
686
    alert.setGraphic( new ImageView( new Image( FILE_LOGO_32 ) ) );
687
    alert.initOwner( getWindow() );
688
689
    alert.showAndWait();
690
  }
691
692
  //---- Member creators ----------------------------------------------------
693
694
  /**
695
   * Factory to create processors that are suited to different file types.
696
   *
697
   * @param tab The tab that is subjected to processing.
698
   * @return A processor suited to the file type specified by the tab's path.
699
   */
700
  private Processor<String> createProcessor( final FileEditorTab tab ) {
701
    return createProcessorFactory().createProcessor( tab );
702
  }
703
704
  private ProcessorFactory createProcessorFactory() {
705
    return new ProcessorFactory( getPreviewPane(), getResolvedMap() );
706
  }
707
708
  private HTMLPreviewPane createHTMLPreviewPane() {
709
    return new HTMLPreviewPane();
710
  }
711
712
  private DefinitionSource createDefaultDefinitionSource() {
713
    return new YamlDefinitionSource( getDefinitionPath() );
714
  }
715
716
  private DefinitionSource createDefinitionSource( final Path path ) {
717
    try {
718
      return createDefinitionFactory().createDefinitionSource( path );
719
    } catch( final Exception ex ) {
720
      error( ex );
721
      return createDefaultDefinitionSource();
722
    }
723
  }
724
725
  private TextField createFindTextField() {
726
    return new TextField();
727
  }
728
729
  /**
730
   * Create an editor pane to hold file editor tabs.
731
   *
732
   * @return A new instance, never null.
733
   */
734
  private FileEditorTabPane createFileEditorPane() {
735
    return new FileEditorTabPane( mScrollEventObserver );
736
  }
737
738
  private DefinitionFactory createDefinitionFactory() {
739
    return new DefinitionFactory();
740
  }
741
742
  private StatusBar createStatusBar() {
743
    return new StatusBar();
744
  }
745
746
  private Scene createScene() {
747
    final SplitPane splitPane = new SplitPane(
748
        getDefinitionPane().getNode(),
749
        getFileEditorPane().getNode(),
750
        getPreviewPane().getNode() );
751
752
    splitPane.setDividerPositions(
753
        getFloat( K_PANE_SPLIT_DEFINITION, .10f ),
754
        getFloat( K_PANE_SPLIT_EDITOR, .45f ),
755
        getFloat( K_PANE_SPLIT_PREVIEW, .45f ) );
756
757
    getDefinitionPane().prefHeightProperty().bind( splitPane.heightProperty() );
758
759
    final BorderPane borderPane = new BorderPane();
760
    borderPane.setPrefSize( 1024, 800 );
761
    borderPane.setTop( createMenuBar() );
762
    borderPane.setBottom( getStatusBar() );
763
    borderPane.setCenter( splitPane );
764
765
    final VBox statusBar = new VBox();
766
    statusBar.setAlignment( Pos.BASELINE_CENTER );
767
    statusBar.getChildren().add( getLineNumberText() );
768
    getStatusBar().getRightItems().add( statusBar );
769
770
    return new Scene( borderPane );
771
  }
772
773
  private Text createLineNumberText() {
774
    return new Text( get( STATUS_BAR_LINE, 1, 1, 1 ) );
775
  }
776
777
  private Node createMenuBar() {
778
    final BooleanBinding activeFileEditorIsNull =
779
        getFileEditorPane().activeFileEditorProperty().isNull();
780
781
    // File actions
782
    final Action fileNewAction = new ActionBuilder()
783
        .setText( "Main.menu.file.new" )
784
        .setAccelerator( "Shortcut+N" )
785
        .setIcon( FILE_ALT )
786
        .setAction( e -> fileNew() )
787
        .build();
788
    final Action fileOpenAction = new ActionBuilder()
789
        .setText( "Main.menu.file.open" )
790
        .setAccelerator( "Shortcut+O" )
791
        .setIcon( FOLDER_OPEN_ALT )
792
        .setAction( e -> fileOpen() )
793
        .build();
794
    final Action fileCloseAction = new ActionBuilder()
795
        .setText( "Main.menu.file.close" )
796
        .setAccelerator( "Shortcut+W" )
797
        .setAction( e -> fileClose() )
798
        .setDisable( activeFileEditorIsNull )
799
        .build();
800
    final Action fileCloseAllAction = new ActionBuilder()
801
        .setText( "Main.menu.file.close_all" )
802
        .setAction( e -> fileCloseAll() )
803
        .setDisable( activeFileEditorIsNull )
804
        .build();
805
    final Action fileSaveAction = new ActionBuilder()
806
        .setText( "Main.menu.file.save" )
807
        .setAccelerator( "Shortcut+S" )
808
        .setIcon( FLOPPY_ALT )
809
        .setAction( e -> fileSave() )
810
        .setDisable( createActiveBooleanProperty(
811
            FileEditorTab::modifiedProperty ).not() )
812
        .build();
813
    final Action fileSaveAsAction = new ActionBuilder()
814
        .setText( "Main.menu.file.save_as" )
815
        .setAction( e -> fileSaveAs() )
816
        .setDisable( activeFileEditorIsNull )
817
        .build();
818
    final Action fileSaveAllAction = new ActionBuilder()
819
        .setText( "Main.menu.file.save_all" )
820
        .setAccelerator( "Shortcut+Shift+S" )
821
        .setAction( e -> fileSaveAll() )
822
        .setDisable( Bindings.not(
823
            getFileEditorPane().anyFileEditorModifiedProperty() ) )
824
        .build();
825
    final Action fileExitAction = new ActionBuilder()
826
        .setText( "Main.menu.file.exit" )
827
        .setAction( e -> fileExit() )
828
        .build();
829
830
    // Edit actions
831
    final Action editUndoAction = new ActionBuilder()
832
        .setText( "Main.menu.edit.undo" )
833
        .setAccelerator( "Shortcut+Z" )
834
        .setIcon( UNDO )
835
        .setAction( e -> getActiveEditor().undo() )
836
        .setDisable( createActiveBooleanProperty(
837
            FileEditorTab::canUndoProperty ).not() )
838
        .build();
839
    final Action editRedoAction = new ActionBuilder()
840
        .setText( "Main.menu.edit.redo" )
841
        .setAccelerator( "Shortcut+Y" )
842
        .setIcon( REPEAT )
843
        .setAction( e -> getActiveEditor().redo() )
844
        .setDisable( createActiveBooleanProperty(
845
            FileEditorTab::canRedoProperty ).not() )
846
        .build();
847
    final Action editFindAction = new ActionBuilder()
848
        .setText( "Main.menu.edit.find" )
849
        .setAccelerator( "Ctrl+F" )
850
        .setIcon( SEARCH )
851
        .setAction( e -> editFind() )
852
        .setDisable( activeFileEditorIsNull )
853
        .build();
854
    final Action editFindNextAction = new ActionBuilder()
855
        .setText( "Main.menu.edit.find.next" )
856
        .setAccelerator( "F3" )
857
        .setIcon( null )
858
        .setAction( e -> editFindNext() )
859
        .setDisable( activeFileEditorIsNull )
860
        .build();
861
    final Action editPreferencesAction = new ActionBuilder()
862
        .setText( "Main.menu.edit.preferences" )
863
        .setAccelerator( "Ctrl+Alt+S" )
864
        .setAction( e -> editPreferences() )
865
        .build();
866
867
    // Insert actions
868
    final Action insertBoldAction = new ActionBuilder()
869
        .setText( "Main.menu.insert.bold" )
870
        .setAccelerator( "Shortcut+B" )
871
        .setIcon( BOLD )
872
        .setAction( e -> insertMarkdown( "**", "**" ) )
873
        .setDisable( activeFileEditorIsNull )
874
        .build();
875
    final Action insertItalicAction = new ActionBuilder()
876
        .setText( "Main.menu.insert.italic" )
877
        .setAccelerator( "Shortcut+I" )
878
        .setIcon( ITALIC )
879
        .setAction( e -> insertMarkdown( "*", "*" ) )
880
        .setDisable( activeFileEditorIsNull )
881
        .build();
882
    final Action insertSuperscriptAction = new ActionBuilder()
883
        .setText( "Main.menu.insert.superscript" )
884
        .setAccelerator( "Shortcut+[" )
885
        .setIcon( SUPERSCRIPT )
886
        .setAction( e -> insertMarkdown( "^", "^" ) )
887
        .setDisable( activeFileEditorIsNull )
888
        .build();
889
    final Action insertSubscriptAction = new ActionBuilder()
890
        .setText( "Main.menu.insert.subscript" )
891
        .setAccelerator( "Shortcut+]" )
892
        .setIcon( SUBSCRIPT )
893
        .setAction( e -> insertMarkdown( "~", "~" ) )
894
        .setDisable( activeFileEditorIsNull )
895
        .build();
896
    final Action insertStrikethroughAction = new ActionBuilder()
897
        .setText( "Main.menu.insert.strikethrough" )
898
        .setAccelerator( "Shortcut+T" )
899
        .setIcon( STRIKETHROUGH )
900
        .setAction( e -> insertMarkdown( "~~", "~~" ) )
901
        .setDisable( activeFileEditorIsNull )
902
        .build();
903
    final Action insertBlockquoteAction = new ActionBuilder()
904
        .setText( "Main.menu.insert.blockquote" )
905
        .setAccelerator( "Ctrl+Q" )
906
        .setIcon( QUOTE_LEFT )
907
        .setAction( e -> insertMarkdown( "\n\n> ", "" ) )
908
        .setDisable( activeFileEditorIsNull )
909
        .build();
910
    final Action insertCodeAction = new ActionBuilder()
911
        .setText( "Main.menu.insert.code" )
912
        .setAccelerator( "Shortcut+K" )
913
        .setIcon( CODE )
914
        .setAction( e -> insertMarkdown( "`", "`" ) )
915
        .setDisable( activeFileEditorIsNull )
916
        .build();
917
    final Action insertFencedCodeBlockAction = new ActionBuilder()
918
        .setText( "Main.menu.insert.fenced_code_block" )
919
        .setAccelerator( "Shortcut+Shift+K" )
920
        .setIcon( FILE_CODE_ALT )
921
        .setAction( e -> getActiveEditor().surroundSelection(
922
            "\n\n```\n",
923
            "\n```\n\n",
924
            get( "Main.menu.insert.fenced_code_block.prompt" ) ) )
925
        .setDisable( activeFileEditorIsNull )
926
        .build();
927
    final Action insertLinkAction = new ActionBuilder()
928
        .setText( "Main.menu.insert.link" )
929
        .setAccelerator( "Shortcut+L" )
930
        .setIcon( LINK )
931
        .setAction( e -> getActiveEditor().insertLink() )
932
        .setDisable( activeFileEditorIsNull )
933
        .build();
934
    final Action insertImageAction = new ActionBuilder()
935
        .setText( "Main.menu.insert.image" )
936
        .setAccelerator( "Shortcut+G" )
937
        .setIcon( PICTURE_ALT )
938
        .setAction( e -> getActiveEditor().insertImage() )
939
        .setDisable( activeFileEditorIsNull )
940
        .build();
941
942
    // Number of header actions (H1 ... H3)
943
    final int HEADERS = 3;
944
    final Action[] headers = new Action[ HEADERS ];
945
946
    for( int i = 1; i <= HEADERS; i++ ) {
947
      final String hashes = new String( new char[ i ] ).replace( "\0", "#" );
948
      final String markup = String.format( "%n%n%s ", hashes );
949
      final String text = "Main.menu.insert.header." + i;
950
      final String accelerator = "Shortcut+" + i;
951
      final String prompt = text + ".prompt";
952
953
      headers[ i - 1 ] = new ActionBuilder()
954
          .setText( text )
955
          .setAccelerator( accelerator )
956
          .setIcon( HEADER )
957
          .setAction( e -> insertMarkdown( markup, "", get( prompt ) ) )
958
          .setDisable( activeFileEditorIsNull )
959
          .build();
960
    }
961
962
    final Action insertUnorderedListAction = new ActionBuilder()
963
        .setText( "Main.menu.insert.unordered_list" )
964
        .setAccelerator( "Shortcut+U" )
965
        .setIcon( LIST_UL )
966
        .setAction( e -> getActiveEditor()
967
            .surroundSelection( "\n\n* ", "" ) )
968
        .setDisable( activeFileEditorIsNull )
969
        .build();
970
    final Action insertOrderedListAction = new ActionBuilder()
971
        .setText( "Main.menu.insert.ordered_list" )
972
        .setAccelerator( "Shortcut+Shift+O" )
973
        .setIcon( LIST_OL )
974
        .setAction( e -> insertMarkdown(
975
            "\n\n1. ", "" ) )
976
        .setDisable( activeFileEditorIsNull )
977
        .build();
978
    final Action insertHorizontalRuleAction = new ActionBuilder()
979
        .setText( "Main.menu.insert.horizontal_rule" )
980
        .setAccelerator( "Shortcut+H" )
981
        .setAction( e -> insertMarkdown(
982
            "\n\n---\n\n", "" ) )
983
        .setDisable( activeFileEditorIsNull )
984
        .build();
985
986
    // Help actions
987
    final Action helpAboutAction = new ActionBuilder()
988
        .setText( "Main.menu.help.about" )
989
        .setAction( e -> helpAbout() )
990
        .build();
991
992
    //---- MenuBar ----
993
    final Menu fileMenu = ActionUtils.createMenu(
994
        get( "Main.menu.file" ),
995
        fileNewAction,
996
        fileOpenAction,
997
        null,
998
        fileCloseAction,
999
        fileCloseAllAction,
1000
        null,
1001
        fileSaveAction,
1002
        fileSaveAsAction,
1003
        fileSaveAllAction,
1004
        null,
1005
        fileExitAction );
1006
1007
    final Menu editMenu = ActionUtils.createMenu(
1008
        get( "Main.menu.edit" ),
1009
        editUndoAction,
1010
        editRedoAction,
1011
        editFindAction,
1012
        editFindNextAction,
1013
        null,
1014
        editPreferencesAction );
1015
1016
    final Menu insertMenu = ActionUtils.createMenu(
1017
        get( "Main.menu.insert" ),
1018
        insertBoldAction,
1019
        insertItalicAction,
1020
        insertSuperscriptAction,
1021
        insertSubscriptAction,
1022
        insertStrikethroughAction,
1023
        insertBlockquoteAction,
1024
        insertCodeAction,
1025
        insertFencedCodeBlockAction,
1026
        null,
1027
        insertLinkAction,
1028
        insertImageAction,
1029
        null,
1030
        headers[ 0 ],
1031
        headers[ 1 ],
1032
        headers[ 2 ],
1033
        null,
1034
        insertUnorderedListAction,
1035
        insertOrderedListAction,
1036
        insertHorizontalRuleAction );
1037
1038
    final Menu helpMenu = ActionUtils.createMenu(
1039
        get( "Main.menu.help" ),
1040
        helpAboutAction );
1041
1042
    final MenuBar menuBar = new MenuBar(
1043
        fileMenu,
1044
        editMenu,
1045
        insertMenu,
1046
        helpMenu );
1047
1048
    //---- ToolBar ----
1049
    final ToolBar toolBar = ActionUtils.createToolBar(
1050
        fileNewAction,
1051
        fileOpenAction,
1052
        fileSaveAction,
1053
        null,
1054
        editUndoAction,
1055
        editRedoAction,
1056
        null,
1057
        insertBoldAction,
1058
        insertItalicAction,
1059
        insertSuperscriptAction,
1060
        insertSubscriptAction,
1061
        insertBlockquoteAction,
1062
        insertCodeAction,
1063
        insertFencedCodeBlockAction,
1064
        null,
1065
        insertLinkAction,
1066
        insertImageAction,
1067
        null,
1068
        headers[ 0 ],
1069
        null,
1070
        insertUnorderedListAction,
1071
        insertOrderedListAction );
1072
1073
    return new VBox( menuBar, toolBar );
1074
  }
1075
1076
  private UserPreferences createUserPreferences() {
1077
    return new UserPreferences();
1078
  }
1079
1080
  /**
1081
   * Creates a boolean property that is bound to another boolean value of the
1082
   * active editor.
1083
   */
1084
  private BooleanProperty createActiveBooleanProperty(
1085
      final Function<FileEditorTab, ObservableBooleanValue> func ) {
1086
1087
    final BooleanProperty b = new SimpleBooleanProperty();
1088
    final FileEditorTab tab = getActiveFileEditor();
1089
1090
    if( tab != null ) {
1091
      b.bind( func.apply( tab ) );
1092
    }
1093
1094
    getFileEditorPane().activeFileEditorProperty().addListener(
1095
        ( observable, oldFileEditor, newFileEditor ) -> {
1096
          b.unbind();
1097
1098
          if( newFileEditor == null ) {
1099
            b.set( false );
1100
          }
1101
          else {
1102
            b.bind( func.apply( newFileEditor ) );
1103
          }
1104
        }
1105
    );
1106
1107
    return b;
1108
  }
1109
1110
  //---- Convenience accessors ----------------------------------------------
1111
1112
  private Preferences getPreferences() {
1113
    return OPTIONS.getState();
1114
  }
1115
1116
  private float getFloat( final String key, final float defaultValue ) {
1117
    return getPreferences().getFloat( key, defaultValue );
1118
  }
1119
1120
  public Window getWindow() {
1121
    return getScene().getWindow();
1122
  }
1123
1124
  private MarkdownEditorPane getActiveEditor() {
1125
    final EditorPane pane = getActiveFileEditor().getEditorPane();
1126
1127
    return pane instanceof MarkdownEditorPane
1128
        ? (MarkdownEditorPane) pane
1129
        : new MarkdownEditorPane();
1130
  }
1131
1132
  private FileEditorTab getActiveFileEditor() {
1133
    return getFileEditorPane().getActiveFileEditor();
1134
  }
1135
1136
  //---- Member accessors ---------------------------------------------------
1137
1138
  protected Scene getScene() {
1139
    return mScene;
1140
  }
1141
1142
  private Map<FileEditorTab, Processor<String>> getProcessors() {
1143
    return mProcessors;
1144
  }
1145
1146
  private FileEditorTabPane getFileEditorPane() {
1147
    if( this.fileEditorPane == null ) {
1148
      this.fileEditorPane = createFileEditorPane();
1149
    }
1150
1151
    return this.fileEditorPane;
1152
  }
1153
1154
  private HTMLPreviewPane getPreviewPane() {
1155
    return mPreviewPane;
1156
  }
1157
1158
  private void setDefinitionSource( final DefinitionSource definitionSource ) {
1159
    assert definitionSource != null;
1160
    mDefinitionSource = definitionSource;
1161
  }
1162
1163
  private DefinitionSource getDefinitionSource() {
1164
    return mDefinitionSource;
1165
  }
1166
1167
  private DefinitionPane getDefinitionPane() {
1168
    return mDefinitionPane;
1169
  }
1170
1171
  private Notifier getNotifier() {
1172
    return NOTIFIER;
1173
  }
1174
1175
  private Text getLineNumberText() {
1176
    return mLineNumberText;
1177
  }
1178
1179
  private StatusBar getStatusBar() {
1180
    return mStatusBar;
1181
  }
1182
1183
  private TextField getFindTextField() {
1184
    return mFindTextField;
1185
  }
1186
1187
  /**
1188
   * Returns the variable map of interpolated definitions.
1189
   *
1190
   * @return A map to help dereference variables.
1191
   */
1192
  private Map<String, String> getResolvedMap() {
1193
    return mResolvedMap;
1194
  }
1195
1196
  //---- Persistence accessors ----------------------------------------------
1197
  private UserPreferences getUserPreferences() {
1198
    return OPTIONS.getUserPreferences();
1199
  }
1200
1201
  private Path getDefinitionPath() {
1202
    return getUserPreferences().getDefinitionPath();
1203
  }
1204
1205
  private File getImagesDirectory() {
1206
    return getUserPreferences().getImagesDirectory();
1207
  }
1208
1209
  private String getImagesOrder() {
1210
    return getUserPreferences().getImagesOrder();
1211
  }
1212
}
11213
A src/main/java/com/scrivenvar/Messages.java
1
/*
2
 * Copyright (c) 2016 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  * Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  * Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar;
28
29
import java.text.MessageFormat;
30
import java.util.ResourceBundle;
31
import java.util.Stack;
32
33
import static com.scrivenvar.Constants.APP_BUNDLE_NAME;
34
import static java.util.ResourceBundle.getBundle;
35
36
/**
37
 * Recursively resolves message properties. Property values can refer to other
38
 * properties using a <code>${var}</code> syntax.
39
 *
40
 * @author Karl Tauber, Dave Jarvis
41
 */
42
public class Messages {
43
44
  private static final ResourceBundle RESOURCE_BUNDLE =
45
      getBundle( APP_BUNDLE_NAME );
46
47
  private Messages() {
48
  }
49
50
  /**
51
   * Return the value of a resource bundle value after having resolved any
52
   * references to other bundle variables.
53
   *
54
   * @param props The bundle containing resolvable properties.
55
   * @param s     The value for a key to resolve.
56
   * @return The value of the key with all references recursively dereferenced.
57
   */
58
  @SuppressWarnings("SameParameterValue")
59
  private static String resolve( final ResourceBundle props, final String s ) {
60
    final int len = s.length();
61
    final Stack<StringBuilder> stack = new Stack<>();
62
63
    StringBuilder sb = new StringBuilder( 256 );
64
    boolean open = false;
65
66
    for( int i = 0; i < len; i++ ) {
67
      final char c = s.charAt( i );
68
69
      switch( c ) {
70
        case '$': {
71
          if( i + 1 < len && s.charAt( i + 1 ) == '{' ) {
72
            stack.push( sb );
73
            sb = new StringBuilder( 256 );
74
            i++;
75
            open = true;
76
          }
77
78
          break;
79
        }
80
81
        case '}': {
82
          if( open ) {
83
            open = false;
84
            final String name = sb.toString();
85
86
            sb = stack.pop();
87
            sb.append( props.getString( name ) );
88
            break;
89
          }
90
        }
91
92
        default: {
93
          sb.append( c );
94
          break;
95
        }
96
      }
97
    }
98
99
    if( open ) {
100
      throw new IllegalArgumentException( "missing '}'" );
101
    }
102
103
    return sb.toString();
104
  }
105
106
  /**
107
   * Returns the value for a key from the message bundle.
108
   *
109
   * @param key Retrieve the value for this key.
110
   * @return The value for the key.
111
   */
112
  public static String get( final String key ) {
113
    try {
114
      return resolve( RESOURCE_BUNDLE, RESOURCE_BUNDLE.getString( key ) );
115
    } catch( final Exception ex ) {
116
      return key;
117
    }
118
  }
119
120
  public static String getLiteral( final String key ) {
121
    return RESOURCE_BUNDLE.getString( key );
122
  }
123
124
  public static String get( final String key, final boolean interpolate ) {
125
    return interpolate ? get( key ) : getLiteral( key );
126
  }
127
128
  /**
129
   * Returns the value for a key from the message bundle with the arguments
130
   * replacing <code>{#}</code> place holders.
131
   *
132
   * @param key  Retrieve the value for this key.
133
   * @param args The values to substitute for place holders.
134
   * @return The value for the key.
135
   */
136
  public static String get( final String key, final Object... args ) {
137
    return MessageFormat.format( get( key ), args );
138
  }
139
}
1140
A src/main/java/com/scrivenvar/Services.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
import java.util.ServiceLoader;
33
34
/**
35
 * Responsible for loading services. The services are treated as singleton
36
 * instances.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class Services {
41
42
  @SuppressWarnings("rawtypes")
43
  private static final Map<Class, Object> SINGLETONS = new HashMap<>();
44
45
  /**
46
   * Loads a service based on its interface definition. This will return an
47
   * existing instance if the class has already been instantiated.
48
   *
49
   * @param <T> The service to load.
50
   * @param api The interface definition for the service.
51
   * @return A class that implements the interface.
52
   */
53
  @SuppressWarnings("unchecked")
54
  public static <T> T load( final Class<T> api ) {
55
    final T o = (T) get( api );
56
57
    return o == null ? newInstance( api ) : o;
58
  }
59
60
  private static <T> T newInstance( final Class<T> api ) {
61
    final ServiceLoader<T> services = ServiceLoader.load( api );
62
63
    for( final T service : services ) {
64
      if( service != null ) {
65
        // Re-use the same instance the next time the class is loaded.
66
        put( api, service );
67
        return service;
68
      }
69
    }
70
71
    throw new RuntimeException( "No implementation for: " + api );
72
  }
73
74
  @SuppressWarnings("rawtypes")
75
  private static void put( final Class key, Object value ) {
76
    SINGLETONS.put( key, value );
77
  }
78
79
  @SuppressWarnings("rawtypes")
80
  private static Object get( final Class api ) {
81
    return SINGLETONS.get( api );
82
  }
83
}
184
A src/main/java/com/scrivenvar/controls/BrowseDirectoryButton.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import com.scrivenvar.Messages;
31
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
32
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
33
import javafx.event.ActionEvent;
34
import javafx.scene.control.Tooltip;
35
import javafx.stage.DirectoryChooser;
36
37
import java.io.File;
38
39
/**
40
 * Button that opens a directory chooser to select a local directory for a
41
 * URL in markdown.
42
 *
43
 * @author Karl Tauber
44
 */
45
public class BrowseDirectoryButton
46
    extends BrowseFileButton {
47
  public BrowseDirectoryButton() {
48
    setGraphic( FontAwesomeIconFactory.get()
49
                                      .createIcon( FontAwesomeIcon.FOLDER_ALT,
50
                                                   "1.2em" ) );
51
    setTooltip( new Tooltip( Messages.get( "BrowseDirectoryButton.tooltip" ) ) );
52
  }
53
54
  @Override
55
  protected void browse( ActionEvent e ) {
56
    DirectoryChooser directoryChooser = new DirectoryChooser();
57
    directoryChooser.setTitle( Messages.get(
58
        "BrowseDirectoryButton.chooser.title" ) );
59
    directoryChooser.setInitialDirectory( getInitialDirectory() );
60
    File result = directoryChooser.showDialog( getScene().getWindow() );
61
    if( result != null ) {
62
      updateUrl( result );
63
    }
64
  }
65
}
166
A src/main/java/com/scrivenvar/controls/BrowseFileButton.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import java.io.File;
31
import java.nio.file.Path;
32
import java.util.ArrayList;
33
import java.util.List;
34
import javafx.beans.property.ObjectProperty;
35
import javafx.beans.property.SimpleObjectProperty;
36
import javafx.event.ActionEvent;
37
import javafx.scene.control.Button;
38
import javafx.scene.control.Tooltip;
39
import javafx.scene.input.KeyCode;
40
import javafx.scene.input.KeyEvent;
41
import javafx.stage.FileChooser;
42
import javafx.stage.FileChooser.ExtensionFilter;
43
import com.scrivenvar.Messages;
44
import de.jensd.fx.glyphs.fontawesome.FontAwesomeIcon;
45
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
46
47
/**
48
 * Button that opens a file chooser to select a local file for a URL in markdown.
49
 *
50
 * @author Karl Tauber
51
 */
52
public class BrowseFileButton
53
	extends Button
54
{
55
	private final List<ExtensionFilter> extensionFilters = new ArrayList<>();
56
57
	public BrowseFileButton() {
58
		setGraphic(FontAwesomeIconFactory.get().createIcon(FontAwesomeIcon.FILE_ALT, "1.2em"));
59
		setTooltip(new Tooltip(Messages.get("BrowseFileButton.tooltip")));
60
		setOnAction(this::browse);
61
62
		disableProperty().bind(basePath.isNull());
63
64
		// workaround for a JavaFX bug:
65
		//   avoid closing the dialog that contains this control when the user
66
		//   closes the FileChooser or DirectoryChooser using the ESC key
67
		addEventHandler(KeyEvent.KEY_RELEASED, e-> {
68
			if (e.getCode() == KeyCode.ESCAPE)
69
				e.consume();
70
		});
71
	}
72
73
	public void addExtensionFilter(ExtensionFilter extensionFilter) {
74
		extensionFilters.add(extensionFilter);
75
	}
76
77
	// 'basePath' property
78
	private final ObjectProperty<Path> basePath = new SimpleObjectProperty<>();
79
	public Path getBasePath() { return basePath.get(); }
80
	public void setBasePath(Path basePath) { this.basePath.set(basePath); }
81
	public ObjectProperty<Path> basePathProperty() { return basePath; }
82
83
	// 'url' property
84
	private final ObjectProperty<String> url = new SimpleObjectProperty<>();
85
	public String getUrl() { return url.get(); }
86
	public void setUrl(String url) { this.url.set(url); }
87
	public ObjectProperty<String> urlProperty() { return url; }
88
89
	protected void browse(ActionEvent e) {
90
		FileChooser fileChooser = new FileChooser();
91
		fileChooser.setTitle(Messages.get("BrowseFileButton.chooser.title"));
92
		fileChooser.getExtensionFilters().addAll(extensionFilters);
93
		fileChooser.getExtensionFilters().add(new ExtensionFilter(Messages.get("BrowseFileButton.chooser.allFilesFilter"), "*.*"));
94
		fileChooser.setInitialDirectory(getInitialDirectory());
95
		File result = fileChooser.showOpenDialog(getScene().getWindow());
96
		if (result != null)
97
			updateUrl(result);
98
	}
99
100
	protected File getInitialDirectory() {
101
		//TODO build initial directory based on current value of 'url' property
102
		return getBasePath().toFile();
103
	}
104
105
	protected void updateUrl(File file) {
106
		String newUrl;
107
		try {
108
			newUrl = getBasePath().relativize(file.toPath()).toString();
109
		} catch (IllegalArgumentException ex) {
110
			newUrl = file.toString();
111
		}
112
		url.set(newUrl.replace('\\', '/'));
113
	}
114
}
1115
A src/main/java/com/scrivenvar/controls/EscapeTextField.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
package com.scrivenvar.controls;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.TextField;
33
import javafx.util.StringConverter;
34
35
/**
36
 * TextField that can escape/unescape characters for markdown.
37
 *
38
 * @author Karl Tauber and White Magic Software, Ltd.
39
 */
40
public class EscapeTextField extends TextField {
41
42
  public EscapeTextField() {
43
    escapedText.bindBidirectional(
44
        textProperty(),
45
        new StringConverter<>() {
46
          @Override
47
          public String toString( String object ) {
48
            return escape( object );
49
          }
50
51
          @Override
52
          public String fromString( String string ) {
53
            return unescape( string );
54
          }
55
        }
56
    );
57
    escapeCharacters.addListener(
58
        e -> escapedText.set( escape( textProperty().get() ) )
59
    );
60
  }
61
62
  // 'escapedText' property
63
  private final StringProperty escapedText = new SimpleStringProperty();
64
65
  public StringProperty escapedTextProperty() {
66
    return escapedText;
67
  }
68
69
  // 'escapeCharacters' property
70
  private final StringProperty escapeCharacters = new SimpleStringProperty();
71
72
  public String getEscapeCharacters() {
73
    return escapeCharacters.get();
74
  }
75
76
  public void setEscapeCharacters( String escapeCharacters ) {
77
    this.escapeCharacters.set( escapeCharacters );
78
  }
79
80
  private String escape( final String s ) {
81
    final String escapeChars = getEscapeCharacters();
82
83
    return isEmpty( escapeChars ) ? s :
84
        s.replaceAll( "([" + escapeChars.replaceAll(
85
            "(.)",
86
            "\\\\$1" ) + "])", "\\\\$1" );
87
  }
88
89
  private String unescape( final String s ) {
90
    final String escapeChars = getEscapeCharacters();
91
92
    return isEmpty( escapeChars ) ? s :
93
        s.replaceAll( "\\\\([" + escapeChars
94
            .replaceAll( "(.)", "\\\\$1" ) + "])", "$1" );
95
  }
96
97
  private static boolean isEmpty( final String s ) {
98
    return s == null || s.isEmpty();
99
  }
100
}
1101
A src/main/java/com/scrivenvar/controls/FlagCheckBox.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.controls;
28
29
import javafx.beans.property.IntegerProperty;
30
import javafx.beans.property.SimpleIntegerProperty;
31
import javafx.scene.control.CheckBox;
32
33
/**
34
 * CheckBox that toggles a bit in an integer.
35
 *
36
 * @author Karl Tauber
37
 */
38
public class FlagCheckBox extends CheckBox {
39
40
  public FlagCheckBox() {
41
    setOnAction( e -> {
42
      if( isSelected() ) {
43
        setFlags( getFlags() | getFlag() );
44
      } else {
45
        setFlags( getFlags() & ~getFlag() );
46
      }
47
    } );
48
49
    flags.addListener( (obs, oldFlags, newFlags) -> {
50
      setSelected( (newFlags.intValue() & getFlag()) != 0 );
51
    } );
52
  }
53
54
  // 'flag' property
55
  private final IntegerProperty flag = new SimpleIntegerProperty();
56
57
  public int getFlag() {
58
    return flag.get();
59
  }
60
61
  public void setFlag( int flag ) {
62
    this.flag.set( flag );
63
  }
64
65
  public IntegerProperty flagProperty() {
66
    return flag;
67
  }
68
69
  // 'flags' property
70
  private final IntegerProperty flags = new SimpleIntegerProperty();
71
72
  public int getFlags() {
73
    return flags.get();
74
  }
75
76
  public void setFlags( int flags ) {
77
    this.flags.set( flags );
78
  }
79
80
  public IntegerProperty flagsProperty() {
81
    return flags;
82
  }
83
}
184
A src/main/java/com/scrivenvar/controls/WebHyperlink.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.controls;
28
29
import com.scrivenvar.Main;
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.scene.control.Hyperlink;
33
34
/**
35
 * Opens a web site in the default web browser.
36
 *
37
 * @author Karl Tauber
38
 */
39
public class WebHyperlink extends Hyperlink {
40
41
  // 'uri' property
42
  private final StringProperty uri = new SimpleStringProperty();
43
44
  public WebHyperlink() {
45
    setStyle( "-fx-padding: 0; -fx-border-width: 0" );
46
  }
47
48
  @Override
49
  public void fire() {
50
    Main.showDocument( getUri() );
51
  }
52
53
  public String getUri() {
54
    return uri.get();
55
  }
56
57
  public void setUri( String uri ) {
58
    this.uri.set( uri );
59
  }
60
61
  public StringProperty uriProperty() {
62
    return uri;
63
  }
64
}
165
A src/main/java/com/scrivenvar/decorators/RVariableDecorator.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Brackets variable names with <code>`r#</code> and <code>`</code>.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class RVariableDecorator implements VariableDecorator {
36
  public static final String PREFIX = "`r#";
37
  public static final char SUFFIX = '`';
38
39
  /**
40
   * Returns the given string R-escaping backticks prepended and appended. This
41
   * is not null safe. Do not pass null into this method.
42
   *
43
   * @param variableName The string to decorate.
44
   * @return "`r#" + variableName + "`".
45
   */
46
  @Override
47
  public String decorate( String variableName ) {
48
    assert variableName != null;
49
50
    // Delete the $ $ sigils from Markdown variables.
51
    if( variableName.length() > 1 ) {
52
      variableName = variableName.substring( 1, variableName.length() - 1 );
53
    }
54
55
    return PREFIX +
56
        "x( v$" +
57
        variableName.replace( '.', '$' ) +
58
        " )" +
59
        SUFFIX;
60
  }
61
}
162
A src/main/java/com/scrivenvar/decorators/VariableDecorator.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Responsible for updating variable names to use a machine-readable format
32
 * corresponding to the type of file being edited.
33
 */
34
public interface VariableDecorator {
35
36
  /**
37
   * This decorates a variable name based on some criteria determined by the
38
   * factory that creates implementations of this interface.
39
   *
40
   * @param variableName The text to decorate as per the filename extension
41
   *                     would indicate (e.g., ".md" goes to $VAR$ while "
42
   *                     .Rmd" goes to `r#VAR`).
43
   * @return The given variable name modified with its requisite delimiters.
44
   */
45
  String decorate( String variableName );
46
}
147
A src/main/java/com/scrivenvar/decorators/YamlVariableDecorator.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.decorators;
29
30
/**
31
 * Brackets variable names with dollar symbols.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class YamlVariableDecorator implements VariableDecorator {
36
37
  /**
38
   * Returns the given {@link String} verbatim because variables in YAML
39
   * documents and plain Markdown documents already have the appropriate
40
   * tokenizable syntax wrapped around the text.
41
   *
42
   * @param variableName Returned verbatim.
43
   */
44
  @Override
45
  public String decorate( final String variableName ) {
46
    assert variableName != null;
47
    return variableName;
48
  }
49
50
  /**
51
   * Sigilifies the given key.
52
   *
53
   * @param key The key to adorn with YAML variable sigil characters.
54
   * @return The given key bracketed by dollar symbols.
55
   */
56
  public static String entoken( final String key ) {
57
    assert key != null;
58
    return '$' + key + '$';
59
  }
60
}
161
A src/main/java/com/scrivenvar/definition/DefinitionFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.FileType;
32
import com.scrivenvar.definition.yaml.YamlDefinitionSource;
33
import com.scrivenvar.util.ProtocolResolver;
34
35
import java.nio.file.Path;
36
37
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_FILE;
38
import static com.scrivenvar.Constants.GLOB_PREFIX_DEFINITION;
39
import static com.scrivenvar.FileType.YAML;
40
41
/**
42
 * Responsible for creating objects that can read and write definition data
43
 * sources. The data source could be YAML, TOML, JSON, flat files, or from a
44
 * database.
45
 *
46
 * @author White Magic Software, Ltd.
47
 */
48
public class DefinitionFactory extends AbstractFileFactory {
49
50
  /**
51
   * Default (empty) constructor.
52
   */
53
  public DefinitionFactory() {
54
  }
55
56
  /**
57
   * Creates a definition source capable of reading definitions from the given
58
   * path.
59
   *
60
   * @param path Path to a resource containing definitions.
61
   * @return The definition source appropriate for the given path.
62
   */
63
  public DefinitionSource createDefinitionSource( final Path path ) {
64
    assert path != null;
65
66
    final String protocol = ProtocolResolver.getProtocol( path.toString() );
67
    DefinitionSource result = null;
68
69
    if( DEFINITION_PROTOCOL_FILE.equals( protocol ) ) {
70
      final FileType filetype = lookup( path, GLOB_PREFIX_DEFINITION );
71
      result = createFileDefinitionSource( filetype, path );
72
    }
73
    else {
74
      unknownFileType( protocol, path.toString() );
75
    }
76
77
    return result;
78
  }
79
80
  /**
81
   * Creates a definition source based on the file type.
82
   *
83
   * @param filetype Property key name suffix from settings.properties file.
84
   * @param path     Path to the file that corresponds to the extension.
85
   * @return A DefinitionSource capable of parsing the data stored at the path.
86
   */
87
  private DefinitionSource createFileDefinitionSource(
88
      final FileType filetype, final Path path ) {
89
    assert filetype != null;
90
    assert path != null;
91
92
    if( filetype == YAML ) {
93
      return new YamlDefinitionSource( path );
94
    }
95
96
    throw new IllegalArgumentException( filetype.toString() );
97
  }
98
}
199
A src/main/java/com/scrivenvar/definition/DefinitionPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.beans.property.SimpleStringProperty;
31
import javafx.beans.property.StringProperty;
32
import javafx.collections.ObservableList;
33
import javafx.event.Event;
34
import javafx.event.EventHandler;
35
import javafx.scene.Node;
36
import javafx.scene.control.*;
37
import javafx.scene.control.cell.TextFieldTreeCell;
38
import javafx.scene.input.KeyEvent;
39
import javafx.util.StringConverter;
40
41
import java.util.*;
42
43
import static com.scrivenvar.Messages.get;
44
import static javafx.scene.input.KeyEvent.KEY_PRESSED;
45
46
/**
47
 * Provides the user interface that holds a {@link TreeView}, which
48
 * allows users to interact with key/value pairs loaded from the
49
 * {@link DocumentParser} and adapted using a {@link TreeAdapter}.
50
 *
51
 * @author White Magic Software, Ltd.
52
 */
53
public final class DefinitionPane extends TitledPane {
54
55
  /**
56
   * Trimmed off the end of a word to match a variable name.
57
   */
58
  private final static String TERMINALS = ":;,.!?-/\\¡¿";
59
60
  /**
61
   * Contains a view of the definitions.
62
   */
63
  private final TreeView<String> mTreeView = new TreeView<>();
64
65
  /**
66
   * Handlers for key press events.
67
   */
68
  private final Set<EventHandler<? super KeyEvent>> mKeyEventHandlers
69
      = new HashSet<>();
70
71
  /**
72
   * Definition file name shown in the title of the pane.
73
   */
74
  private final StringProperty mFilename = new SimpleStringProperty();
75
76
  /**
77
   * Constructs a definition pane with a given tree view root.
78
   */
79
  public DefinitionPane() {
80
    final var treeView = getTreeView();
81
    treeView.setEditable( true );
82
    treeView.setCellFactory( cell -> createTreeCell() );
83
    treeView.setContextMenu( createContextMenu() );
84
    treeView.addEventFilter( KEY_PRESSED, this::keyEventFilter );
85
    treeView.setShowRoot( false );
86
    getSelectionModel().setSelectionMode( SelectionMode.MULTIPLE );
87
88
    textProperty().bind( mFilename );
89
90
    setContent( treeView );
91
    setCollapsible( false );
92
  }
93
94
  /**
95
   * Changes the root of the {@link TreeView} to the root of the
96
   * {@link TreeView} from the {@link DefinitionSource}.
97
   *
98
   * @param definitionSource Container for the hierarchy of key/value pairs
99
   *                         to replace the existing hierarchy.
100
   */
101
  public void update( final DefinitionSource definitionSource ) {
102
    assert definitionSource != null;
103
104
    final TreeAdapter treeAdapter = definitionSource.getTreeAdapter();
105
    final TreeItem<String> root = treeAdapter.adapt(
106
        get( "Pane.definition.node.root.title" )
107
    );
108
109
    getTreeView().setRoot( root );
110
  }
111
112
  public Map<String, String> toMap() {
113
    return TreeItemAdapter.toMap( getTreeView().getRoot() );
114
  }
115
116
  /**
117
   * Informs the caller of whenever any {@link TreeItem} in the {@link TreeView}
118
   * is modified. The modifications include: item value changes, item additions,
119
   * and item removals.
120
   * <p>
121
   * Safe to call multiple times; if a handler is already registered, the
122
   * old handler is used.
123
   * </p>
124
   *
125
   * @param handler The handler to call whenever any {@link TreeItem} changes.
126
   */
127
  public void addTreeChangeHandler(
128
      final EventHandler<TreeItem.TreeModificationEvent<Event>> handler ) {
129
    final TreeItem<String> root = getTreeView().getRoot();
130
    root.addEventHandler( TreeItem.valueChangedEvent(), handler );
131
    root.addEventHandler( TreeItem.childrenModificationEvent(), handler );
132
  }
133
134
  public void addKeyEventHandler(
135
      final EventHandler<? super KeyEvent> handler ) {
136
    getKeyEventHandlers().add( handler );
137
  }
138
139
  /**
140
   * Answers whether the {@link TreeItem}s in the {@link TreeView} are suitably
141
   * well-formed for export. A tree is considered well-formed if the following
142
   * conditions are met:
143
   *
144
   * <ul>
145
   *   <li>The root node contains at least one child node having a leaf.</li>
146
   *   <li>There are no leaf nodes with sibling leaf nodes.</li>
147
   * </ul>
148
   *
149
   * @return {@code null} if the document is well-formed, otherwise the
150
   * problematic child {@link TreeItem}.
151
   */
152
  public TreeItem<String> isTreeWellFormed() {
153
    final var root = getTreeView().getRoot();
154
155
    for( final var child : root.getChildren() ) {
156
      final var problemChild = isWellFormed( child );
157
158
      if( child.isLeaf() || problemChild != null ) {
159
        return problemChild;
160
      }
161
    }
162
163
    return null;
164
  }
165
166
  /**
167
   * Determines whether the document is well-formed by ensuring that
168
   * child branches do not contain multiple leaves.
169
   *
170
   * @param item The sub-tree to check for well-formedness.
171
   * @return {@code null} when the tree is well-formed, otherwise the
172
   * problematic {@link TreeItem}.
173
   */
174
  private TreeItem<String> isWellFormed( final TreeItem<String> item ) {
175
    int childLeafs = 0;
176
    int childBranches = 0;
177
178
    for( final TreeItem<String> child : item.getChildren() ) {
179
      if( child.isLeaf() ) {
180
        childLeafs++;
181
      }
182
      else {
183
        childBranches++;
184
      }
185
186
      final var problemChild = isWellFormed( child );
187
188
      if( problemChild != null ) {
189
        return problemChild;
190
      }
191
    }
192
193
    return ((childBranches > 0 && childLeafs == 0) ||
194
        (childBranches == 0 && childLeafs <= 1)) ? null : item;
195
  }
196
197
  /**
198
   * Returns the leaf that matches the given value. If the value is terminally
199
   * punctuated, the punctuation is removed if no match was found.
200
   *
201
   * @param value    The value to find, never null.
202
   * @param findMode Defines how to match words.
203
   * @return The leaf that contains the given value, or null if neither the
204
   * original value nor the terminally-trimmed value was found.
205
   */
206
  public VariableTreeItem<String> findLeaf(
207
      final String value, final FindMode findMode ) {
208
    final VariableTreeItem<String> root = getTreeRoot();
209
    final VariableTreeItem<String> leaf = root.findLeaf( value, findMode );
210
211
    return leaf == null
212
        ? root.findLeaf( rtrimTerminalPunctuation( value ) )
213
        : leaf;
214
  }
215
216
  /**
217
   * Removes punctuation from the end of a string.
218
   *
219
   * @param s The string to trim, never null.
220
   * @return The string trimmed of all terminal characters from the end
221
   */
222
  private String rtrimTerminalPunctuation( final String s ) {
223
    assert s != null;
224
    int index = s.length() - 1;
225
226
    while( index > 0 && (TERMINALS.indexOf( s.charAt( index ) ) >= 0) ) {
227
      index--;
228
    }
229
230
    return s.substring( 0, index );
231
  }
232
233
  /**
234
   * Expands the node to the root, recursively.
235
   *
236
   * @param <T>  The type of tree item to expand (usually String).
237
   * @param node The node to expand.
238
   */
239
  public <T> void expand( final TreeItem<T> node ) {
240
    if( node != null ) {
241
      expand( node.getParent() );
242
243
      if( !node.isLeaf() ) {
244
        node.setExpanded( true );
245
      }
246
    }
247
  }
248
249
  public void select( final TreeItem<String> item ) {
250
    getSelectionModel().clearSelection();
251
    getSelectionModel().select( getTreeView().getRow( item ) );
252
  }
253
254
  /**
255
   * Collapses the tree, recursively.
256
   */
257
  public void collapse() {
258
    collapse( getTreeRoot().getChildren() );
259
  }
260
261
  /**
262
   * Collapses the tree, recursively.
263
   *
264
   * @param <T>   The type of tree item to expand (usually String).
265
   * @param nodes The nodes to collapse.
266
   */
267
  private <T> void collapse( final ObservableList<TreeItem<T>> nodes ) {
268
    for( final TreeItem<T> node : nodes ) {
269
      node.setExpanded( false );
270
      collapse( node.getChildren() );
271
    }
272
  }
273
274
  /**
275
   * @return {@code true} when the user is editing a {@link TreeItem}.
276
   */
277
  private boolean isEditingTreeItem() {
278
    return getTreeView().editingItemProperty().getValue() != null;
279
  }
280
281
  /**
282
   * Changes to edit mode for the selected item.
283
   */
284
  private void editSelectedItem() {
285
    getTreeView().edit( getSelectedItem() );
286
  }
287
288
  /**
289
   * Removes all selected items from the {@link TreeView}.
290
   */
291
  private void deleteSelectedItems() {
292
    for( final TreeItem<String> item : getSelectedItems() ) {
293
      final TreeItem<String> parent = item.getParent();
294
295
      if( parent != null ) {
296
        parent.getChildren().remove( item );
297
      }
298
    }
299
  }
300
301
  /**
302
   * Deletes the selected item.
303
   */
304
  private void deleteSelectedItem() {
305
    final TreeItem<String> c = getSelectedItem();
306
    getSiblings( c ).remove( c );
307
  }
308
309
  /**
310
   * Adds a new item under the selected item (or root if nothing is selected).
311
   * There are a few conditions to consider: when adding to the root,
312
   * when adding to a leaf, and when adding to a non-leaf. Items added to the
313
   * root must contain two items: a key and a value.
314
   */
315
  private void addItem() {
316
    final TreeItem<String> value = createTreeItem();
317
    getSelectedItem().getChildren().add( value );
318
    expand( value );
319
    select( value );
320
  }
321
322
  private ContextMenu createContextMenu() {
323
    final ContextMenu menu = new ContextMenu();
324
    final ObservableList<MenuItem> items = menu.getItems();
325
326
    addMenuItem( items, "Definition.menu.create" )
327
        .setOnAction( e -> addItem() );
328
329
    addMenuItem( items, "Definition.menu.rename" )
330
        .setOnAction( e -> editSelectedItem() );
331
332
    addMenuItem( items, "Definition.menu.remove" )
333
        .setOnAction( e -> deleteSelectedItem() );
334
335
    return menu;
336
  }
337
338
  /**
339
   * Executes hot-keys for edits to the definition tree.
340
   *
341
   * @param event Contains the key code of the key that was pressed.
342
   */
343
  private void keyEventFilter( final KeyEvent event ) {
344
    if( !isEditingTreeItem() ) {
345
      switch( event.getCode() ) {
346
        case ENTER:
347
          expand( getSelectedItem() );
348
          event.consume();
349
          break;
350
351
        case DELETE:
352
          deleteSelectedItems();
353
          break;
354
355
        case INSERT:
356
          addItem();
357
          break;
358
359
        case R:
360
          if( event.isControlDown() ) {
361
            editSelectedItem();
362
          }
363
364
          break;
365
      }
366
367
      for( final var handler : getKeyEventHandlers() ) {
368
        handler.handle( event );
369
      }
370
    }
371
  }
372
373
  /**
374
   * Adds a menu item to a list of menu items.
375
   *
376
   * @param items    The list of menu items to append to.
377
   * @param labelKey The resource bundle key name for the menu item's label.
378
   * @return The menu item added to the list of menu items.
379
   */
380
  private MenuItem addMenuItem(
381
      final List<MenuItem> items, final String labelKey ) {
382
    final MenuItem menuItem = createMenuItem( labelKey );
383
    items.add( menuItem );
384
    return menuItem;
385
  }
386
387
  private MenuItem createMenuItem( final String labelKey ) {
388
    return new MenuItem( get( labelKey ) );
389
  }
390
391
  private VariableTreeItem<String> createTreeItem() {
392
    return new VariableTreeItem<>( get( "Definition.menu.add.default" ) );
393
  }
394
395
  private TreeCell<String> createTreeCell() {
396
    return new TextFieldTreeCell<>(
397
        createStringConverter() ) {
398
      @Override
399
      public void commitEdit( final String newValue ) {
400
        super.commitEdit( newValue );
401
        select( getTreeItem() );
402
        requestFocus();
403
      }
404
    };
405
  }
406
407
  @Override
408
  public void requestFocus() {
409
    super.requestFocus();
410
    getTreeView().requestFocus();
411
  }
412
413
  private StringConverter<String> createStringConverter() {
414
    return new StringConverter<>() {
415
      @Override
416
      public String toString( final String object ) {
417
        return object == null ? "" : object;
418
      }
419
420
      @Override
421
      public String fromString( final String string ) {
422
        return string == null ? "" : string;
423
      }
424
    };
425
  }
426
427
  /**
428
   * Returns the tree view that contains the definition hierarchy.
429
   *
430
   * @return A non-null instance.
431
   */
432
  public TreeView<String> getTreeView() {
433
    return mTreeView;
434
  }
435
436
  /**
437
   * Returns this pane.
438
   *
439
   * @return this
440
   */
441
  public Node getNode() {
442
    return this;
443
  }
444
445
  /**
446
   * Returns the property used to set the title of the pane: the file name.
447
   *
448
   * @return A non-null property used for showing the definition file name.
449
   */
450
  public StringProperty filenameProperty() {
451
    return mFilename;
452
  }
453
454
  /**
455
   * Returns the root of the tree.
456
   *
457
   * @return The first node added to the definition tree.
458
   */
459
  private VariableTreeItem<String> getTreeRoot() {
460
    final TreeItem<String> root = getTreeView().getRoot();
461
462
    return root instanceof VariableTreeItem ?
463
        (VariableTreeItem<String>) root : new VariableTreeItem<>( "root" );
464
  }
465
466
  private ObservableList<TreeItem<String>> getSiblings(
467
      final TreeItem<String> item ) {
468
    final TreeItem<String> root = getTreeView().getRoot();
469
    final TreeItem<String> parent =
470
        (item == null || item == root) ? root : item.getParent();
471
472
    return parent.getChildren();
473
  }
474
475
  private MultipleSelectionModel<TreeItem<String>> getSelectionModel() {
476
    return getTreeView().getSelectionModel();
477
  }
478
479
  /**
480
   * Returns a copy of all the selected items.
481
   *
482
   * @return A list, possibly empty, containing all selected items in the
483
   * {@link TreeView}.
484
   */
485
  private List<TreeItem<String>> getSelectedItems() {
486
    return new ArrayList<>( getSelectionModel().getSelectedItems() );
487
  }
488
489
  public TreeItem<String> getSelectedItem() {
490
    final TreeItem<String> item = getSelectionModel().getSelectedItem();
491
    return item == null ? getTreeView().getRoot() : item;
492
  }
493
494
  private Set<EventHandler<? super KeyEvent>> getKeyEventHandlers() {
495
    return mKeyEventHandlers;
496
  }
497
}
1498
A src/main/java/com/scrivenvar/definition/DefinitionSource.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
/**
31
 * Represents behaviours for reading and writing string definitions. This
32
 * class cannot have any direct hooks into the user interface, as it defines
33
 * entry points into the definition data model loaded into an object
34
 * hierarchy. That hierarchy is converted to a UI model using an adapter
35
 * pattern.
36
 */
37
public interface DefinitionSource {
38
39
  /**
40
   * Creates an object capable of producing view-based objects from this
41
   * definition source.
42
   *
43
   * @return A hierarchical tree suitable for displaying in the definition pane.
44
   */
45
  TreeAdapter getTreeAdapter();
46
47
  /**
48
   * Returns the error message, if any, that occurred while loading the
49
   * definition source.
50
   *
51
   * @return The empty string if no error occurred, otherwise the error message.
52
   */
53
  default String getError() {
54
    return "";
55
  }
56
}
157
A src/main/java/com/scrivenvar/definition/DocumentParser.java
1
package com.scrivenvar.definition;
2
3
/**
4
 * Responsible for parsing structured document formats.
5
 *
6
 * @param <T> The type of "node" for the document's object model.
7
 */
8
public interface DocumentParser<T> {
9
10
  /**
11
   * Parses a document into a nested object hierarchy. The object returned
12
   * from this call must be the root node in the document tree.
13
   *
14
   * @return The document's root node, which may be empty but never null.
15
   */
16
  T getDocumentRoot();
17
}
118
A src/main/java/com/scrivenvar/definition/FindMode.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
/**
31
 * Used to find variable keys by matching values. The values are matched
32
 * according to the relationships provided in this enumeration.
33
 */
34
public enum FindMode {
35
  EXACT,
36
  CONTAINS,
37
  STARTS_WITH,
38
  LEVENSHTEIN
39
}
40
141
A src/main/java/com/scrivenvar/definition/MapInterpolator.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import java.util.Map;
31
import java.util.regex.Matcher;
32
import java.util.regex.Pattern;
33
34
/**
35
 * Responsible for performing string interpolation on key/value pairs stored
36
 * in a map. The values in the map can use a delimited syntax to refer to
37
 * keys in the map.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public class MapInterpolator {
42
43
  /**
44
   * Matches variables delimited by dollar symbols.
45
   */
46
  private final static String REGEX = "(\\$.*?\\$)";
47
48
  /**
49
   * Compiled regular expression for matching delimited references.
50
   */
51
  private final static Pattern REGEX_PATTERN = Pattern.compile( REGEX );
52
53
  private final static int GROUP_DELIMITED = 1;
54
55
  /**
56
   * Empty.
57
   */
58
  private MapInterpolator() {
59
  }
60
61
  /**
62
   * Performs string interpolation on the values in the given map. This will
63
   * change any value in the map that contains a variable that matches
64
   * {@link #REGEX_PATTERN}.
65
   *
66
   * @param map Contains values that represent references to keys.
67
   */
68
  public static void interpolate( final Map<String, String> map ) {
69
    map.replaceAll( ( k, v ) -> resolve( map, v ) );
70
  }
71
72
  /**
73
   * Given a value with zero or more key references, this will resolve all
74
   * the values, recursively. If a key cannot be dereferenced, the value will
75
   * contain the key name.
76
   *
77
   * @param map   Map to search for keys when resolving key references.
78
   * @param value Value containing zero or more key references
79
   * @return The given value with all embedded key references interpolated.
80
   */
81
  private static String resolve(
82
      final Map<String, String> map, String value ) {
83
    final Matcher matcher = REGEX_PATTERN.matcher( value );
84
85
    while( matcher.find() ) {
86
      final String keyName = matcher.group( GROUP_DELIMITED );
87
      final String keyValue = resolve(
88
          map, map.getOrDefault( keyName, keyName )
89
      );
90
91
      value = value.replace( keyName, keyValue );
92
    }
93
94
    return value;
95
  }
96
}
197
A src/main/java/com/scrivenvar/definition/RootTreeItem.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeItem;
31
import javafx.scene.control.TreeView;
32
33
/**
34
 * Indicates that this is the top-most {@link TreeItem}. This class allows
35
 * the {@link TreeItemAdapter} to ignore the topmost definition. Such
36
 * contortions are necessary because {@link TreeView} requires a root item
37
 * that isn't part of the user's definition file.
38
 * <p>
39
 * Another approach would be to associate object pairs per {@link TreeItem},
40
 * but that would be a waste of memory since the only "exception" case is
41
 * the root {@link TreeItem}.
42
 * </p>
43
 *
44
 * @param <T> The type of {@link TreeItem} to store in the {@link TreeView}.
45
 * @author White Magic Software, Ltd.
46
 */
47
public class RootTreeItem<T> extends VariableTreeItem<T> {
48
  /**
49
   * Default constructor, calls the superclass, no other behaviour.
50
   *
51
   * @param value The {@link TreeItem} node name to construct the superclass.
52
   * @see TreeItemAdapter#toMap(TreeItem) for details on how this
53
   * class is used.
54
   */
55
  public RootTreeItem( final T value ) {
56
    super( value );
57
  }
58
}
159
A src/main/java/com/scrivenvar/definition/TreeAdapter.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeItem;
31
32
import java.io.IOException;
33
import java.nio.file.Path;
34
35
/**
36
 * Responsible for converting an object hierarchy into a {@link TreeItem}
37
 * hierarchy.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public interface TreeAdapter {
42
  /**
43
   * Adapts the document produced by the given parser into a {@link TreeItem}
44
   * object that can be presented to the user within a GUI.
45
   *
46
   * @param root The default root node name.
47
   * @return The parsed document in a {@link TreeItem} that can be displayed
48
   * in a panel.
49
   */
50
  TreeItem<String> adapt( String root );
51
52
  /**
53
   * Exports the given root node to the given path.
54
   *
55
   * @param root The root node to export.
56
   * @param path Where to persist the data.
57
   * @throws IOException Could not write the data to the given path.
58
   */
59
  void export( TreeItem<String> root, Path path ) throws IOException;
60
}
161
A src/main/java/com/scrivenvar/definition/TreeItemAdapter.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.scrivenvar.decorators.YamlVariableDecorator;
32
import com.scrivenvar.preview.HTMLPreviewPane;
33
import javafx.scene.control.TreeItem;
34
import javafx.scene.control.TreeView;
35
36
import java.util.HashMap;
37
import java.util.Iterator;
38
import java.util.Map;
39
import java.util.Stack;
40
41
import static com.scrivenvar.Constants.DEFAULT_MAP_SIZE;
42
43
/**
44
 * Given a {@link TreeItem}, this will generate a flat map with all the
45
 * values in the tree recursively interpolated. The application integrates
46
 * definition files as follows:
47
 * <ol>
48
 *   <li>Load YAML file into {@link JsonNode} hierarchy.</li>
49
 *   <li>Convert JsonNode to a {@link TreeItem} hierarchy.</li>
50
 *   <li>Interpolate {@link TreeItem} hierarchy as a flat map.</li>
51
 *   <li>Substitute flat map variables into document as required.</li>
52
 * </ol>
53
 *
54
 * <p>
55
 * This class is responsible for producing the interpolated flat map. This
56
 * allows dynamic edits of the {@link TreeView} to be displayed in the
57
 * {@link HTMLPreviewPane} without having to reload the definition file.
58
 * Reloading the definition file would work, but has a number of drawbacks.
59
 * </p>
60
 *
61
 * @author White Magic Software, Ltd.
62
 */
63
public class TreeItemAdapter {
64
  /**
65
   * Separates YAML variable nodes (e.g., the dots in {@code $root.node.var$}).
66
   */
67
  public static final String SEPARATOR = ".";
68
69
  /**
70
   * Default buffer length for keys ({@link StringBuilder} has 16 character
71
   * buffer) that should be large enough for most keys to avoid reallocating
72
   * memory to increase the {@link StringBuilder}'s buffer.
73
   */
74
  public static final int DEFAULT_KEY_LENGTH = 64;
75
76
  /**
77
   * In-order traversal of a {@link TreeItem} hierarchy, exposing each item
78
   * as a consecutive list.
79
   */
80
  private static final class TreeIterator
81
      implements Iterator<TreeItem<String>> {
82
    private final Stack<TreeItem<String>> mStack = new Stack<>();
83
84
    public TreeIterator( final TreeItem<String> root ) {
85
      if( root != null ) {
86
        mStack.push( root );
87
      }
88
    }
89
90
    @Override
91
    public boolean hasNext() {
92
      return !mStack.isEmpty();
93
    }
94
95
    @Override
96
    public TreeItem<String> next() {
97
      final TreeItem<String> next = mStack.pop();
98
      next.getChildren().forEach( mStack::push );
99
100
      return next;
101
    }
102
  }
103
104
  private TreeItemAdapter() {
105
  }
106
107
  /**
108
   * Iterate over a given root node (at any level of the tree) and process each
109
   * leaf node into a flat map. Values must be interpolated separately.
110
   */
111
  public static Map<String, String> toMap( final TreeItem<String> root ) {
112
    final Map<String, String> map = new HashMap<>( DEFAULT_MAP_SIZE );
113
    final TreeIterator iterator = new TreeIterator( root );
114
115
    iterator.forEachRemaining( item -> {
116
      if( item.isLeaf() ) {
117
        map.put( toPath( item.getParent() ), item.getValue() );
118
      }
119
    } );
120
121
    return map;
122
  }
123
124
125
  /**
126
   * For a given node, this will ascend the tree to generate a key name
127
   * that is associated with the leaf node's value.
128
   *
129
   * @param node Ascendants represent the key to this node's value.
130
   * @param <T>  Data type that the {@link TreeItem} contains.
131
   * @return The string representation of the node's unique key.
132
   */
133
  public static <T> String toPath( TreeItem<T> node ) {
134
    assert node != null;
135
136
    final StringBuilder key = new StringBuilder( DEFAULT_KEY_LENGTH );
137
    final Stack<TreeItem<T>> stack = new Stack<>();
138
139
    while( node != null && !(node instanceof RootTreeItem) ) {
140
      stack.push( node );
141
      node = node.getParent();
142
    }
143
144
    // Gets set at end of first iteration (to avoid an if condition).
145
    String separator = "";
146
147
    while( !stack.empty() ) {
148
      final T subkey = stack.pop().getValue();
149
      key.append( separator );
150
      key.append( subkey );
151
      separator = SEPARATOR;
152
    }
153
154
    return YamlVariableDecorator.entoken( key.toString() );
155
  }
156
}
1157
A src/main/java/com/scrivenvar/definition/VariableTreeItem.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeItem;
31
32
import java.text.Normalizer;
33
import java.util.Stack;
34
35
import static com.scrivenvar.definition.FindMode.*;
36
import static java.text.Normalizer.Form.NFD;
37
38
/**
39
 * Provides behaviour afforded to variable names and their corresponding value.
40
 *
41
 * @param <T> The type of TreeItem (usually String).
42
 * @author White Magic Software, Ltd.
43
 */
44
public class VariableTreeItem<T> extends TreeItem<T> {
45
46
  /**
47
   * Constructs a new item with a default value.
48
   *
49
   * @param value Passed up to superclass.
50
   */
51
  public VariableTreeItem( final T value ) {
52
    super( value );
53
  }
54
55
  /**
56
   * Finds a leaf starting at the current node with text that matches the given
57
   * value.
58
   *
59
   * @param text The text to match against each leaf in the tree.
60
   * @return The leaf that has a value starting with the given text.
61
   */
62
  public VariableTreeItem<T> findLeaf( final String text ) {
63
    return findLeaf( text, STARTS_WITH );
64
  }
65
66
  /**
67
   * Finds a leaf starting at the current node with text that matches the given
68
   * value.
69
   *
70
   * @param text     The text to match against each leaf in the tree.
71
   * @param findMode What algorithm is used to match the given text.
72
   * @return The leaf that has a value starting with the given text.
73
   */
74
  public VariableTreeItem<T> findLeaf(
75
      final String text, final FindMode findMode ) {
76
    final Stack<VariableTreeItem<T>> stack = new Stack<>();
77
    final VariableTreeItem<T> root = this;
78
79
    stack.push( root );
80
81
    // Don't try to find keys for blank/empty variable values.
82
    boolean found = text.isBlank();
83
    VariableTreeItem<T> node = null;
84
85
    while( !found && !stack.isEmpty() ) {
86
      node = stack.pop();
87
88
      if( findMode == EXACT && node.valueEquals( text ) ) {
89
        found = true;
90
      }
91
      else if( findMode == CONTAINS && node.valueContains( text ) ) {
92
        found = true;
93
      }
94
      else if( findMode == STARTS_WITH && node.valueStartsWith( text ) ) {
95
        found = true;
96
      }
97
      else {
98
        for( final TreeItem<T> child : node.getChildren() ) {
99
          stack.push( (VariableTreeItem<T>) child );
100
        }
101
102
        // No match found, yet.
103
        node = null;
104
      }
105
    }
106
107
    return node;
108
  }
109
110
  /**
111
   * Returns the value of the string without diacritic marks.
112
   *
113
   * @return A non-null, possibly empty string.
114
   */
115
  private String getDiacriticlessValue() {
116
    final String value = getValue().toString();
117
    final String normalized = Normalizer.normalize( value, NFD );
118
119
    return normalized.replaceAll( "\\p{M}", "" );
120
  }
121
122
  /**
123
   * Returns true if this node is a leaf and its value starts with the given
124
   * text.
125
   *
126
   * @param s The text to compare against the node value.
127
   * @return true Node is a leaf and its value starts with the given value.
128
   */
129
  private boolean valueStartsWith( final String s ) {
130
    return isLeaf() && getDiacriticlessValue().startsWith( s );
131
  }
132
133
  /**
134
   * Returns true if this node is a leaf and its value contains the given text.
135
   *
136
   * @param s The text to compare against the node value.
137
   * @return true Node is a leaf and its value contains the given value.
138
   */
139
  private boolean valueContains( final String s ) {
140
    return isLeaf() && getDiacriticlessValue().contains( s );
141
  }
142
143
  /**
144
   * Returns true if this node is a leaf and its value equals the given text.
145
   *
146
   * @param s The text to compare against the node value.
147
   * @return true Node is a leaf and its value equals the given value.
148
   */
149
  private boolean valueEquals( final String s ) {
150
    return isLeaf() && getValue().equals( s );
151
  }
152
153
  /**
154
   * Returns the path for this node, with nodes made distinct using the
155
   * separator character. This uses two loops: one for pushing nodes onto a
156
   * stack and one for popping them off to create the path in desired order.
157
   *
158
   * @return A non-null string, possibly empty.
159
   */
160
  public String toPath() {
161
    return TreeItemAdapter.toPath( getParent() );
162
  }
163
}
1164
A src/main/java/com/scrivenvar/definition/yaml/YamlDefinitionSource.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.scrivenvar.definition.DefinitionSource;
31
import com.scrivenvar.definition.TreeAdapter;
32
33
import java.nio.file.Path;
34
35
/**
36
 * Represents a definition data source for YAML files.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class YamlDefinitionSource implements DefinitionSource {
41
42
  private final YamlTreeAdapter mYamlTreeAdapter;
43
44
  /**
45
   * Constructs a new YAML definition source, populated from the given file.
46
   *
47
   * @param path Path to the YAML definition file.
48
   */
49
  public YamlDefinitionSource( final Path path ) {
50
    assert path != null;
51
52
    mYamlTreeAdapter = new YamlTreeAdapter( path );
53
  }
54
55
  @Override
56
  public TreeAdapter getTreeAdapter() {
57
    return mYamlTreeAdapter;
58
  }
59
60
  @Override
61
  public String getError() {
62
    return getYamlParser().getError();
63
  }
64
65
  private YamlParser getYamlParser() {
66
    return mYamlTreeAdapter.getYamlParser();
67
  }
68
}
169
A src/main/java/com/scrivenvar/definition/yaml/YamlParser.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.ObjectMapper;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
33
import com.scrivenvar.Messages;
34
import com.scrivenvar.definition.DocumentParser;
35
36
import java.io.InputStream;
37
import java.nio.file.Files;
38
import java.nio.file.Path;
39
40
import static com.scrivenvar.Constants.STATUS_BAR_OK;
41
42
/**
43
 * Responsible for reading a YAML document into an object hierarchy.
44
 *
45
 * @author White Magic Software, Ltd.
46
 */
47
public class YamlParser implements DocumentParser<JsonNode> {
48
49
  /**
50
   * Error that occurred while parsing.
51
   */
52
  private String mError;
53
54
  /**
55
   * Start of the Universe (the YAML document node that contains all others).
56
   */
57
  private final JsonNode mDocumentRoot;
58
59
  /**
60
   * Creates a new YamlParser instance that attempts to parse the contents
61
   * of the YAML document given from a path. In the event that the file either
62
   * does not exist or is empty, a fake
63
   *
64
   * @param path Path to a file containing YAML data to parse.
65
   */
66
  public YamlParser( final Path path ) {
67
    assert path != null;
68
    mDocumentRoot = parse( path );
69
  }
70
71
  /**
72
   * Returns the parent node for the entire YAML document tree.
73
   *
74
   * @return The document root, never {@code null}.
75
   */
76
  @Override
77
  public JsonNode getDocumentRoot() {
78
    return mDocumentRoot;
79
  }
80
81
  /**
82
   * Parses the given path containing YAML data into an object hierarchy.
83
   *
84
   * @param path {@link Path} to the YAML resource to parse.
85
   * @return The parsed contents, or an empty object hierarchy.
86
   */
87
  private JsonNode parse( final Path path ) {
88
    try( final InputStream in = Files.newInputStream( path ) ) {
89
      setError( Messages.get( STATUS_BAR_OK ) );
90
91
      return new ObjectMapper( new YAMLFactory() ).readTree( in );
92
    } catch( final Exception e ) {
93
      setError( Messages.get( "yaml.error.open" ) );
94
95
      // Ensure that a document root node exists by relying on the
96
      // default failure condition when processing. This is required
97
      // because the input stream could not be read.
98
      return new ObjectMapper().createObjectNode();
99
    }
100
  }
101
102
  private void setError( final String error ) {
103
    mError = error;
104
  }
105
106
  /**
107
   * Returns the last error message, if any, that occurred during parsing.
108
   *
109
   * @return The error message or the empty string if no error occurred.
110
   */
111
  public String getError() {
112
    return mError;
113
  }
114
}
1115
A src/main/java/com/scrivenvar/definition/yaml/YamlTreeAdapter.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition.yaml;
29
30
import com.fasterxml.jackson.databind.JsonNode;
31
import com.fasterxml.jackson.databind.node.ObjectNode;
32
import com.fasterxml.jackson.dataformat.yaml.YAMLMapper;
33
import com.scrivenvar.definition.RootTreeItem;
34
import com.scrivenvar.definition.TreeAdapter;
35
import com.scrivenvar.definition.VariableTreeItem;
36
import javafx.scene.control.TreeItem;
37
import javafx.scene.control.TreeView;
38
39
import java.io.IOException;
40
import java.nio.file.Path;
41
import java.util.Map.Entry;
42
43
/**
44
 * Transforms a JsonNode hierarchy into a tree that can be displayed in a user
45
 * interface and vice-versa.
46
 *
47
 * @author White Magic Software, Ltd.
48
 */
49
public class YamlTreeAdapter implements TreeAdapter {
50
  private final YamlParser mParser;
51
52
  /**
53
   * Constructs a new instance that will use the given path to read
54
   * the object hierarchy from a data source.
55
   *
56
   * @param path Path to YAML contents to parse.
57
   */
58
  public YamlTreeAdapter( final Path path ) {
59
    mParser = new YamlParser( path );
60
  }
61
62
  @Override
63
  public void export( final TreeItem<String> treeItem, final Path path )
64
      throws IOException {
65
    final YAMLMapper mapper = new YAMLMapper();
66
    final ObjectNode root = mapper.createObjectNode();
67
68
    // Iterate over the root item's children. The root item is used by the
69
    // application to ensure definitions can always be added to a tree, as
70
    // such it is not meant to be exported, only its children.
71
    for( final TreeItem<String> child : treeItem.getChildren() ) {
72
      export( child, root );
73
    }
74
75
    // Writes as UTF8 by default.
76
    mapper.writeValue( path.toFile(), root );
77
  }
78
79
  /**
80
   * Recursive method to generate an object hierarchy that represents the
81
   * given {@link TreeItem} hierarchy.
82
   *
83
   * @param item The {@link TreeItem} to reproduce as an object hierarchy.
84
   * @param node The {@link ObjectNode} to update to reflect the
85
   *             {@link TreeItem} hierarchy.
86
   */
87
  private void export( final TreeItem<String> item, ObjectNode node ) {
88
    final var children = item.getChildren();
89
90
    // If the current item has more than one non-leaf child, it's an
91
    // object node and must become a new nested object.
92
    if( !(children.size() == 1 && children.get( 0 ).isLeaf()) ) {
93
      node = node.putObject( item.getValue() );
94
    }
95
96
    for( final TreeItem<String> child : children ) {
97
      if( child.isLeaf() ) {
98
        node.put( item.getValue(), child.getValue() );
99
      }
100
      else {
101
        export( child, node );
102
      }
103
    }
104
  }
105
106
  /**
107
   * Converts a YAML document to a {@link TreeItem} based on the document
108
   * keys. Only the first document in the stream is adapted.
109
   *
110
   * @param root Root {@link TreeItem} node name.
111
   * @return A {@link TreeItem} populated with all the keys in the YAML
112
   * document.
113
   */
114
  public TreeItem<String> adapt( final String root ) {
115
    final JsonNode rootNode = getYamlParser().getDocumentRoot();
116
    final TreeItem<String> rootItem = createRootTreeItem( root );
117
118
    rootItem.setExpanded( true );
119
    adapt( rootNode, rootItem );
120
    return rootItem;
121
  }
122
123
  /**
124
   * Iterate over a given root node (at any level of the tree) and adapt each
125
   * leaf node.
126
   *
127
   * @param rootNode A JSON node (YAML node) to adapt.
128
   * @param rootItem The tree item to use as the root when processing the node.
129
   */
130
  private void adapt(
131
      final JsonNode rootNode, final TreeItem<String> rootItem ) {
132
    rootNode.fields().forEachRemaining(
133
        ( Entry<String, JsonNode> leaf ) -> adapt( leaf, rootItem )
134
    );
135
  }
136
137
  /**
138
   * Recursively adapt each rootNode to a corresponding rootItem.
139
   *
140
   * @param rootNode The node to adapt.
141
   * @param rootItem The item to adapt using the node's key.
142
   */
143
  private void adapt(
144
      final Entry<String, JsonNode> rootNode,
145
      final TreeItem<String> rootItem ) {
146
    final JsonNode leafNode = rootNode.getValue();
147
    final String key = rootNode.getKey();
148
    final TreeItem<String> leaf = createTreeItem( key );
149
150
    if( leafNode.isValueNode() ) {
151
      leaf.getChildren().add( createTreeItem( rootNode.getValue().asText() ) );
152
    }
153
154
    rootItem.getChildren().add( leaf );
155
156
    if( leafNode.isObject() ) {
157
      adapt( leafNode, leaf );
158
    }
159
  }
160
161
  /**
162
   * Creates a new {@link TreeItem} that can be added to the {@link TreeView}.
163
   *
164
   * @param value The node's value.
165
   * @return A new {@link TreeItem}, never {@code null}.
166
   */
167
  private TreeItem<String> createTreeItem( final String value ) {
168
    return new VariableTreeItem<>( value );
169
  }
170
171
  /**
172
   * Creates a new {@link TreeItem} that is intended to be the root-level item
173
   * added to the {@link TreeView}. This allows the root item to be
174
   * distinguished from the other items so that reference keys do not include
175
   * "Definition" as part of their name.
176
   *
177
   * @param value The node's value.
178
   * @return A new {@link TreeItem}, never {@code null}.
179
   */
180
  private TreeItem<String> createRootTreeItem( final String value ) {
181
    return new RootTreeItem<>( value );
182
  }
183
184
  public YamlParser getYamlParser() {
185
    return mParser;
186
  }
187
}
1188
A src/main/java/com/scrivenvar/dialogs/AbstractDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import static com.scrivenvar.Messages.get;
31
import com.scrivenvar.service.events.impl.ButtonOrderPane;
32
import static javafx.scene.control.ButtonType.CANCEL;
33
import static javafx.scene.control.ButtonType.OK;
34
import javafx.scene.control.Dialog;
35
import javafx.stage.Window;
36
37
/**
38
 * Superclass that abstracts common behaviours for all dialogs.
39
 *
40
 * @author White Magic Software, Ltd.
41
 * @param <T> The type of dialog to create (usually String).
42
 */
43
public abstract class AbstractDialog<T> extends Dialog<T> {
44
45
  /**
46
   * Ensures that all dialogs can be closed.
47
   *
48
   * @param owner The parent window of this dialog.
49
   * @param title The messages title to display in the title bar.
50
   */
51
  @SuppressWarnings( "OverridableMethodCallInConstructor" )
52
  public AbstractDialog( final Window owner, final String title ) {
53
    setTitle( get( title ) );
54
    setResizable( true );
55
56
    initOwner( owner );
57
    initCloseAction();
58
    initDialogPane();
59
    initDialogButtons();
60
    initComponents();
61
  }
62
63
  /**
64
   * Initialize the component layout.
65
   */
66
  protected abstract void initComponents();
67
68
  /**
69
   * Set the dialog to use a button order pane with an OK and a CANCEL button.
70
   */
71
  protected void initDialogPane() {
72
    setDialogPane( new ButtonOrderPane() );
73
  }
74
  
75
  /**
76
   * Set an OK and CANCEL button on the dialog.
77
   */
78
  protected void initDialogButtons() {
79
    getDialogPane().getButtonTypes().addAll( OK, CANCEL );
80
  }
81
82
  /**
83
   * Attaches a setOnCloseRequest to the dialog's [X] button so that the user
84
   * can always close the window, even if there's an error.
85
   */
86
  protected final void initCloseAction() {
87
    final Window window = getDialogPane().getScene().getWindow();
88
    window.setOnCloseRequest( event -> window.hide() );
89
  }
90
}
191
A src/main/java/com/scrivenvar/dialogs/ImageDialog.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.dialogs;
28
29
import static com.scrivenvar.Messages.get;
30
import com.scrivenvar.controls.BrowseFileButton;
31
import com.scrivenvar.controls.EscapeTextField;
32
import java.nio.file.Path;
33
import javafx.application.Platform;
34
import javafx.beans.binding.Bindings;
35
import javafx.beans.property.SimpleStringProperty;
36
import javafx.beans.property.StringProperty;
37
import javafx.scene.control.ButtonBar.ButtonData;
38
import static javafx.scene.control.ButtonType.OK;
39
import javafx.scene.control.DialogPane;
40
import javafx.scene.control.Label;
41
import javafx.stage.FileChooser.ExtensionFilter;
42
import javafx.stage.Window;
43
import org.tbee.javafx.scene.layout.fxml.MigPane;
44
45
/**
46
 * Dialog to enter a markdown image.
47
 *
48
 * @author Karl Tauber
49
 */
50
public class ImageDialog extends AbstractDialog<String> {
51
52
  private final StringProperty image = new SimpleStringProperty();
53
54
  public ImageDialog( final Window owner, final Path basePath ) {
55
    super(owner, "Dialog.image.title" );
56
    
57
    final DialogPane dialogPane = getDialogPane();
58
    dialogPane.setContent( pane );
59
60
    linkBrowseFileButton.setBasePath( basePath );
61
    linkBrowseFileButton.addExtensionFilter( new ExtensionFilter( get( "Dialog.image.chooser.imagesFilter" ), "*.png", "*.gif", "*.jpg" ) );
62
    linkBrowseFileButton.urlProperty().bindBidirectional( urlField.escapedTextProperty() );
63
64
    dialogPane.lookupButton( OK ).disableProperty().bind(
65
      urlField.escapedTextProperty().isEmpty()
66
      .or( textField.escapedTextProperty().isEmpty() ) );
67
68
    image.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
69
      .then( Bindings.format( "![%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
70
      .otherwise( Bindings.format( "![%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) ) );
71
    previewField.textProperty().bind( image );
72
73
    setResultConverter( dialogButton -> {
74
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
75
      return (data == ButtonData.OK_DONE) ? image.get() : null;
76
    } );
77
78
    Platform.runLater( () -> {
79
      urlField.requestFocus();
80
81
      if( urlField.getText().startsWith( "http://" ) ) {
82
        urlField.selectRange( "http://".length(), urlField.getLength() );
83
      }
84
    } );
85
  }
86
87
  @Override
88
  protected void initComponents() {
89
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
90
    pane = new MigPane();
91
    Label urlLabel = new Label();
92
    urlField = new EscapeTextField();
93
    linkBrowseFileButton = new BrowseFileButton();
94
    Label textLabel = new Label();
95
    textField = new EscapeTextField();
96
    Label titleLabel = new Label();
97
    titleField = new EscapeTextField();
98
    Label previewLabel = new Label();
99
    previewField = new Label();
100
101
    //======== pane ========
102
    {
103
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill]" );
104
      pane.setRows( "[][][][]" );
105
106
      //---- urlLabel ----
107
      urlLabel.setText( get( "Dialog.image.urlLabel.text" ) );
108
      pane.add( urlLabel, "cell 0 0" );
109
110
      //---- urlField ----
111
      urlField.setEscapeCharacters( "()" );
112
      urlField.setText( "http://yourlink.com" );
113
      urlField.setPromptText( "http://yourlink.com" );
114
      pane.add( urlField, "cell 1 0" );
115
      pane.add( linkBrowseFileButton, "cell 2 0" );
116
117
      //---- textLabel ----
118
      textLabel.setText( get( "Dialog.image.textLabel.text" ) );
119
      pane.add( textLabel, "cell 0 1" );
120
121
      //---- textField ----
122
      textField.setEscapeCharacters( "[]" );
123
      pane.add( textField, "cell 1 1 2 1" );
124
125
      //---- titleLabel ----
126
      titleLabel.setText( get( "Dialog.image.titleLabel.text" ) );
127
      pane.add( titleLabel, "cell 0 2" );
128
      pane.add( titleField, "cell 1 2 2 1" );
129
130
      //---- previewLabel ----
131
      previewLabel.setText( get( "Dialog.image.previewLabel.text" ) );
132
      pane.add( previewLabel, "cell 0 3" );
133
      pane.add( previewField, "cell 1 3 2 1" );
134
    }
135
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
136
  }
137
138
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
139
  private MigPane pane;
140
  private EscapeTextField urlField;
141
  private BrowseFileButton linkBrowseFileButton;
142
  private EscapeTextField textField;
143
  private EscapeTextField titleField;
144
  private Label previewField;
145
	// JFormDesigner - End of variables declaration  //GEN-END:variables
146
}
1147
A src/main/java/com/scrivenvar/dialogs/ImageDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "ImageDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "ImageDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
34
				name: "linkBrowseFileButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "javafx.scene.control.Label" ) {
39
				name: "textLabel"
40
				"text": new FormMessage( null, "ImageDialog.textLabel.text" )
41
				auxiliary() {
42
					"JavaCodeGenerator.variableLocal": true
43
				}
44
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
45
				"value": "cell 0 1"
46
			} )
47
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
48
				name: "textField"
49
				"escapeCharacters": "[]"
50
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
51
				"value": "cell 1 1 2 1"
52
			} )
53
			add( new FormComponent( "javafx.scene.control.Label" ) {
54
				name: "titleLabel"
55
				"text": new FormMessage( null, "ImageDialog.titleLabel.text" )
56
				auxiliary() {
57
					"JavaCodeGenerator.variableLocal": true
58
				}
59
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
60
				"value": "cell 0 2"
61
			} )
62
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
63
				name: "titleField"
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 1 2 2 1"
66
			} )
67
			add( new FormComponent( "javafx.scene.control.Label" ) {
68
				name: "previewLabel"
69
				"text": new FormMessage( null, "ImageDialog.previewLabel.text" )
70
				auxiliary() {
71
					"JavaCodeGenerator.variableLocal": true
72
				}
73
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
74
				"value": "cell 0 3"
75
			} )
76
			add( new FormComponent( "javafx.scene.control.Label" ) {
77
				name: "previewField"
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 1 3 2 1"
80
			} )
81
		}, new FormLayoutConstraints( null ) {
82
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
83
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
84
		} )
85
	}
86
}
187
A src/main/java/com/scrivenvar/dialogs/LinkDialog.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import com.scrivenvar.controls.EscapeTextField;
31
import com.scrivenvar.editors.markdown.HyperlinkModel;
32
import javafx.application.Platform;
33
import javafx.beans.binding.Bindings;
34
import javafx.beans.property.SimpleStringProperty;
35
import javafx.beans.property.StringProperty;
36
import javafx.scene.control.ButtonBar.ButtonData;
37
import javafx.scene.control.DialogPane;
38
import javafx.scene.control.Label;
39
import javafx.stage.Window;
40
import org.tbee.javafx.scene.layout.fxml.MigPane;
41
42
import static com.scrivenvar.Messages.get;
43
import static javafx.scene.control.ButtonType.OK;
44
45
/**
46
 * Dialog to enter a markdown link.
47
 *
48
 * @author Karl Tauber
49
 */
50
public class LinkDialog extends AbstractDialog<String> {
51
52
  private final StringProperty link = new SimpleStringProperty();
53
54
  public LinkDialog(
55
    final Window owner, final HyperlinkModel hyperlink ) {
56
    super( owner, "Dialog.link.title" );
57
58
    final DialogPane dialogPane = getDialogPane();
59
    dialogPane.setContent( pane );
60
61
    dialogPane.lookupButton( OK ).disableProperty().bind(
62
      urlField.escapedTextProperty().isEmpty() );
63
64
    textField.setText( hyperlink.getText() );
65
    urlField.setText( hyperlink.getUrl() );
66
    titleField.setText( hyperlink.getTitle() );
67
68
    link.bind( Bindings.when( titleField.escapedTextProperty().isNotEmpty() )
69
      .then( Bindings.format( "[%s](%s \"%s\")", textField.escapedTextProperty(), urlField.escapedTextProperty(), titleField.escapedTextProperty() ) )
70
      .otherwise( Bindings.when( textField.escapedTextProperty().isNotEmpty() )
71
        .then( Bindings.format( "[%s](%s)", textField.escapedTextProperty(), urlField.escapedTextProperty() ) )
72
        .otherwise( urlField.escapedTextProperty() ) ) );
73
74
    setResultConverter( dialogButton -> {
75
      ButtonData data = (dialogButton != null) ? dialogButton.getButtonData() : null;
76
      return (data == ButtonData.OK_DONE) ? link.get() : null;
77
    } );
78
79
    Platform.runLater( () -> {
80
      urlField.requestFocus();
81
      urlField.selectRange( 0, urlField.getLength() );
82
    } );
83
  }
84
85
  @Override
86
  protected void initComponents() {
87
    // JFormDesigner - Component initialization - DO NOT MODIFY  //GEN-BEGIN:initComponents
88
    pane = new MigPane();
89
    Label urlLabel = new Label();
90
    urlField = new EscapeTextField();
91
    Label textLabel = new Label();
92
    textField = new EscapeTextField();
93
    Label titleLabel = new Label();
94
    titleField = new EscapeTextField();
95
96
    //======== pane ========
97
    {
98
      pane.setCols( "[shrink 0,fill][300,grow,fill][fill][fill]" );
99
      pane.setRows( "[][][][]" );
100
101
      //---- urlLabel ----
102
      urlLabel.setText( get( "Dialog.link.urlLabel.text" ) );
103
      pane.add( urlLabel, "cell 0 0" );
104
105
      //---- urlField ----
106
      urlField.setEscapeCharacters( "()" );
107
      pane.add( urlField, "cell 1 0" );
108
109
      //---- textLabel ----
110
      textLabel.setText( get( "Dialog.link.textLabel.text" ) );
111
      pane.add( textLabel, "cell 0 1" );
112
113
      //---- textField ----
114
      textField.setEscapeCharacters( "[]" );
115
      pane.add( textField, "cell 1 1 3 1" );
116
117
      //---- titleLabel ----
118
      titleLabel.setText( get( "Dialog.link.titleLabel.text" ) );
119
      pane.add( titleLabel, "cell 0 2" );
120
      pane.add( titleField, "cell 1 2 3 1" );
121
    }
122
    // JFormDesigner - End of component initialization  //GEN-END:initComponents
123
  }
124
125
  // JFormDesigner - Variables declaration - DO NOT MODIFY  //GEN-BEGIN:variables
126
  private MigPane pane;
127
  private EscapeTextField urlField;
128
  private EscapeTextField textField;
129
  private EscapeTextField titleField;
130
  // JFormDesigner - End of variables declaration  //GEN-END:variables
131
}
1132
A src/main/java/com/scrivenvar/dialogs/LinkDialog.jfd
1
JFDML JFormDesigner: "9.9.9.9.9999" Java: "1.8.0_66" encoding: "UTF-8"
2
3
new FormModel {
4
	"i18n.bundlePackage": "com.scrivendor"
5
	"i18n.bundleName": "messages"
6
	"i18n.autoExternalize": true
7
	"i18n.keyPrefix": "LinkDialog"
8
	contentType: "form/javafx"
9
	root: new FormRoot {
10
		add( new FormContainer( "org.tbee.javafx.scene.layout.fxml.MigPane", new FormLayoutManager( class org.tbee.javafx.scene.layout.fxml.MigPane ) {
11
			"$layoutConstraints": ""
12
			"$columnConstraints": "[shrink 0,fill][300,grow,fill][fill][fill]"
13
			"$rowConstraints": "[][][][]"
14
		} ) {
15
			name: "pane"
16
			add( new FormComponent( "javafx.scene.control.Label" ) {
17
				name: "urlLabel"
18
				"text": new FormMessage( null, "LinkDialog.urlLabel.text" )
19
				auxiliary() {
20
					"JavaCodeGenerator.variableLocal": true
21
				}
22
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
23
				"value": "cell 0 0"
24
			} )
25
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
26
				name: "urlField"
27
				"escapeCharacters": "()"
28
				"text": "http://yourlink.com"
29
				"promptText": "http://yourlink.com"
30
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
31
				"value": "cell 1 0"
32
			} )
33
			add( new FormComponent( "com.scrivendor.controls.BrowseDirectoryButton" ) {
34
				name: "linkBrowseDirectoyButton"
35
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
36
				"value": "cell 2 0"
37
			} )
38
			add( new FormComponent( "com.scrivendor.controls.BrowseFileButton" ) {
39
				name: "linkBrowseFileButton"
40
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
41
				"value": "cell 3 0"
42
			} )
43
			add( new FormComponent( "javafx.scene.control.Label" ) {
44
				name: "textLabel"
45
				"text": new FormMessage( null, "LinkDialog.textLabel.text" )
46
				auxiliary() {
47
					"JavaCodeGenerator.variableLocal": true
48
				}
49
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
50
				"value": "cell 0 1"
51
			} )
52
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
53
				name: "textField"
54
				"escapeCharacters": "[]"
55
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
56
				"value": "cell 1 1 3 1"
57
			} )
58
			add( new FormComponent( "javafx.scene.control.Label" ) {
59
				name: "titleLabel"
60
				"text": new FormMessage( null, "LinkDialog.titleLabel.text" )
61
				auxiliary() {
62
					"JavaCodeGenerator.variableLocal": true
63
				}
64
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
65
				"value": "cell 0 2"
66
			} )
67
			add( new FormComponent( "com.scrivendor.controls.EscapeTextField" ) {
68
				name: "titleField"
69
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
70
				"value": "cell 1 2 3 1"
71
			} )
72
			add( new FormComponent( "javafx.scene.control.Label" ) {
73
				name: "previewLabel"
74
				"text": new FormMessage( null, "LinkDialog.previewLabel.text" )
75
				auxiliary() {
76
					"JavaCodeGenerator.variableLocal": true
77
				}
78
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
79
				"value": "cell 0 3"
80
			} )
81
			add( new FormComponent( "javafx.scene.control.Label" ) {
82
				name: "previewField"
83
			}, new FormLayoutConstraints( class net.miginfocom.layout.CC ) {
84
				"value": "cell 1 3 3 1"
85
			} )
86
		}, new FormLayoutConstraints( null ) {
87
			"location": new javafx.geometry.Point2D( 0.0, 0.0 )
88
			"size": new javafx.geometry.Dimension2D( 500.0, 300.0 )
89
		} )
90
	}
91
}
192
A src/main/java/com/scrivenvar/dialogs/RScriptDialog.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.dialogs;
29
30
import javafx.application.Platform;
31
import javafx.geometry.Insets;
32
import javafx.scene.control.Label;
33
import javafx.scene.control.TextArea;
34
import javafx.scene.layout.GridPane;
35
import javafx.stage.Window;
36
37
import static com.scrivenvar.Messages.get;
38
import static javafx.scene.control.ButtonType.OK;
39
40
/**
41
 * Responsible for managing the R startup script that is run when an R source
42
 * file is loaded.
43
 *
44
 * @author White Magic Software, Ltd.
45
 */
46
public class RScriptDialog extends AbstractDialog<String> {
47
48
  private TextArea mScriptArea;
49
  private final String mOriginalText;
50
51
  public RScriptDialog(
52
      final Window parent, final String title, final String script ) {
53
    super( parent, title );
54
    mOriginalText = script;
55
    getScriptArea().setText( script );
56
  }
57
58
  @Override
59
  protected void initComponents() {
60
    final GridPane grid = new GridPane();
61
    grid.setHgap( 10 );
62
    grid.setVgap( 10 );
63
    grid.setPadding( new Insets( 10, 10, 10, 10 ) );
64
65
    final Label label = new Label( get( "Dialog.r.script.content" ) );
66
67
    final TextArea textArea = getScriptArea();
68
    textArea.setEditable( true );
69
    textArea.setWrapText( true );
70
71
    grid.add( label, 0, 0 );
72
    grid.add( textArea, 0, 1 );
73
74
    getDialogPane().setContent( grid );
75
76
    Platform.runLater( textArea::requestFocus );
77
78
    setResultConverter(
79
        dialogButton -> dialogButton == OK ?
80
            textArea.getText() :
81
            getOriginalText()
82
    );
83
  }
84
85
  private TextArea getScriptArea() {
86
    if( mScriptArea == null ) {
87
      mScriptArea = new TextArea();
88
    }
89
90
    return mScriptArea;
91
  }
92
93
  private String getOriginalText() {
94
    return mOriginalText;
95
  }
96
}
197
A src/main/java/com/scrivenvar/editors/EditorPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.AbstractPane;
31
import javafx.application.Platform;
32
import javafx.beans.property.ObjectProperty;
33
import javafx.beans.property.SimpleObjectProperty;
34
import javafx.beans.value.ChangeListener;
35
import javafx.event.Event;
36
import javafx.scene.control.ScrollPane;
37
import org.fxmisc.flowless.VirtualizedScrollPane;
38
import org.fxmisc.richtext.StyleClassedTextArea;
39
import org.fxmisc.undo.UndoManager;
40
import org.fxmisc.wellbehaved.event.EventPattern;
41
import org.fxmisc.wellbehaved.event.Nodes;
42
43
import java.nio.file.Path;
44
import java.util.function.Consumer;
45
46
import static org.fxmisc.wellbehaved.event.InputMap.consume;
47
48
/**
49
 * Represents common editing features for various types of text editors.
50
 *
51
 * @author White Magic Software, Ltd.
52
 */
53
public class EditorPane extends AbstractPane {
54
55
  private final StyleClassedTextArea mEditor =
56
      new StyleClassedTextArea( false );
57
  private final VirtualizedScrollPane<StyleClassedTextArea> mScrollPane =
58
      new VirtualizedScrollPane<>( mEditor );
59
  private final ObjectProperty<Path> mPath = new SimpleObjectProperty<>();
60
61
  public EditorPane() {
62
    getScrollPane().setVbarPolicy( ScrollPane.ScrollBarPolicy.ALWAYS );
63
  }
64
65
  @Override
66
  public void requestFocus() {
67
    Platform.runLater( () -> getEditor().requestFocus() );
68
  }
69
70
  public void undo() {
71
    getUndoManager().undo();
72
  }
73
74
  public void redo() {
75
    getUndoManager().redo();
76
  }
77
78
  public UndoManager<?> getUndoManager() {
79
    return getEditor().getUndoManager();
80
  }
81
82
  public String getText() {
83
    return getEditor().getText();
84
  }
85
86
  public void setText( final String text ) {
87
    final var editor = getEditor();
88
    editor.deselect();
89
    editor.replaceText( text );
90
    getUndoManager().mark();
91
  }
92
93
  /**
94
   * Call to hook into changes to the text area.
95
   *
96
   * @param listener Receives editor text change events.
97
   */
98
  public void addTextChangeListener(
99
      final ChangeListener<? super String> listener ) {
100
    getEditor().textProperty().addListener( listener );
101
  }
102
103
  /**
104
   * Call to listen for when the caret moves to another paragraph.
105
   *
106
   * @param listener Receives paragraph change events.
107
   */
108
  public void addCaretParagraphListener(
109
      final ChangeListener<? super Integer> listener ) {
110
    getEditor().currentParagraphProperty().addListener( listener );
111
  }
112
113
  /**
114
   * This method adds listeners to editor events.
115
   *
116
   * @param <T>      The event type.
117
   * @param <U>      The consumer type for the given event type.
118
   * @param event    The event of interest.
119
   * @param consumer The method to call when the event happens.
120
   */
121
  public <T extends Event, U extends T> void addKeyboardListener(
122
      final EventPattern<? super T, ? extends U> event,
123
      final Consumer<? super U> consumer ) {
124
    Nodes.addInputMap( getEditor(), consume( event, consumer ) );
125
  }
126
127
  /**
128
   * Repositions the cursor and scroll bar to the top of the file.
129
   */
130
  public void scrollToTop() {
131
    getEditor().moveTo( 0 );
132
    getScrollPane().scrollYToPixel( 0 );
133
  }
134
135
  public StyleClassedTextArea getEditor() {
136
    return mEditor;
137
  }
138
139
  /**
140
   * Returns the scroll pane that contains the text area.
141
   *
142
   * @return The scroll pane that contains the content to edit.
143
   */
144
  public VirtualizedScrollPane<StyleClassedTextArea> getScrollPane() {
145
    return mScrollPane;
146
  }
147
148
  public Path getPath() {
149
    return mPath.get();
150
  }
151
152
  public void setPath( final Path path ) {
153
    mPath.set( path );
154
  }
155
}
1156
A src/main/java/com/scrivenvar/editors/VariableNameDecoratorFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.decorators.RVariableDecorator;
32
import com.scrivenvar.decorators.VariableDecorator;
33
import com.scrivenvar.decorators.YamlVariableDecorator;
34
import java.nio.file.Path;
35
36
/**
37
 * Responsible for creating a variable name decorator suited to a particular
38
 * file type.
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public class VariableNameDecoratorFactory extends AbstractFileFactory {
43
44
  private VariableNameDecoratorFactory() {
45
  }
46
47
  public static VariableDecorator newInstance( final Path path ) {
48
    final var factory = new VariableNameDecoratorFactory();
49
    final VariableDecorator result;
50
51
    switch( factory.lookup( path ) ) {
52
      case RMARKDOWN:
53
      case RXML:
54
        result = new RVariableDecorator();
55
        break;
56
57
      default:
58
        result = new YamlVariableDecorator();
59
        break;
60
    }
61
62
    return result;
63
  }
64
}
165
A src/main/java/com/scrivenvar/editors/VariableNameInjector.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors;
29
30
import com.scrivenvar.FileEditorTab;
31
import com.scrivenvar.decorators.VariableDecorator;
32
import com.scrivenvar.definition.DefinitionPane;
33
import com.scrivenvar.definition.FindMode;
34
import com.scrivenvar.definition.VariableTreeItem;
35
import javafx.event.Event;
36
import javafx.scene.control.TreeItem;
37
import javafx.scene.input.KeyEvent;
38
import org.fxmisc.richtext.StyledTextArea;
39
import org.fxmisc.wellbehaved.event.EventPattern;
40
41
import java.nio.file.Path;
42
import java.text.BreakIterator;
43
import java.util.function.Consumer;
44
45
import static com.scrivenvar.definition.FindMode.*;
46
import static javafx.scene.input.KeyCode.SPACE;
47
import static javafx.scene.input.KeyCombination.CONTROL_DOWN;
48
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
49
50
/**
51
 * Provides the logic for injecting variable names within the editor.
52
 *
53
 * @author White Magic Software, Ltd.
54
 */
55
public final class VariableNameInjector {
56
57
  /**
58
   * Recipient of name injections.
59
   */
60
  private FileEditorTab mTab;
61
62
  /**
63
   * Initiates double-click events.
64
   */
65
  private DefinitionPane mDefinitionPane;
66
67
  /**
68
   * Initializes the variable name injector against the given pane.
69
   *
70
   * @param tab  The tab to inject variable names into.
71
   * @param pane The definition panel to listen to for double-click events.
72
   */
73
  public VariableNameInjector(
74
      final FileEditorTab tab, final DefinitionPane pane ) {
75
    setFileEditorTab( tab );
76
    setDefinitionPane( pane );
77
    initKeyboardEventListeners();
78
  }
79
80
  /**
81
   * Trap control+space and the @ key.
82
   *
83
   * @param tab The file editor that sends keyboard events for variable name
84
   *            injection.
85
   */
86
  public void initKeyboardEventListeners( final FileEditorTab tab ) {
87
    setFileEditorTab( tab );
88
    initKeyboardEventListeners();
89
  }
90
91
  /**
92
   * Inserts the variable
93
   */
94
  public void injectSelectedItem() {
95
    final TreeItem<String> item = getDefinitionPane().getSelectedItem();
96
97
    if( item.isLeaf() ) {
98
      // This avoids a direct typecast.
99
      final VariableTreeItem<String> leaf = getDefinitionPane().findLeaf(
100
          item.getValue(), FindMode.EXACT );
101
      final StyledTextArea<?, ?> editor = getEditor();
102
103
      editor.insertText( editor.getCaretPosition(), decorate( leaf ) );
104
    }
105
  }
106
107
  /**
108
   * Traps Control+SPACE to auto-insert definition key names.
109
   */
110
  private void initKeyboardEventListeners() {
111
    addKeyboardListener(
112
        keyPressed( SPACE, CONTROL_DOWN ),
113
        this::autoinsert
114
    );
115
  }
116
117
  /**
118
   * Pressing Control+SPACE will find a node that matches the current word and
119
   * substitute the YAML variable reference.
120
   *
121
   * @param e Ignored -- it can only be Control+SPACE.
122
   */
123
  private void autoinsert( final KeyEvent e ) {
124
    final String paragraph = getCaretParagraph();
125
    final int[] boundaries = getWordBoundariesAtCaret();
126
    final String word = paragraph.substring( boundaries[ 0 ], boundaries[ 1 ] );
127
    final VariableTreeItem<String> leaf = findLeaf( word );
128
129
    if( leaf != null ) {
130
      replaceText( boundaries[ 0 ], boundaries[ 1 ], decorate( leaf ) );
131
      expand( leaf );
132
    }
133
  }
134
135
  private int[] getWordBoundariesAtCaret() {
136
    final String paragraph = getCaretParagraph();
137
    int offset = getCurrentCaretColumn();
138
139
    final BreakIterator wordBreaks = BreakIterator.getWordInstance();
140
    wordBreaks.setText( paragraph );
141
142
    // Scan back until the first word is found.
143
    while( offset > 0 && wordBreaks.isBoundary( offset ) ) {
144
      offset--;
145
    }
146
147
    final int[] boundaries = new int[ 2 ];
148
    boundaries[ 1 ] = wordBreaks.following( offset );
149
    boundaries[ 0 ] = wordBreaks.previous();
150
151
    return boundaries;
152
  }
153
154
  /**
155
   * Decorates a {@link TreeItem} using the syntax specific to the type of
156
   * document being edited.
157
   *
158
   * @param leaf The path to the leaf (the definition key) to be decorated.
159
   */
160
  private String decorate( final VariableTreeItem<String> leaf ) {
161
    return decorate( leaf.toPath() );
162
  }
163
164
  /**
165
   * Decorates a variable using the syntax specific to the type of document
166
   * being edited.
167
   *
168
   * @param variable The variable to decorate in dot-notation without any
169
   *                 start or end sigils present.
170
   */
171
  private String decorate( final String variable ) {
172
    return getVariableDecorator().decorate( variable );
173
  }
174
175
  /**
176
   * Updates the text at the given position within the current paragraph.
177
   *
178
   * @param posBegan The starting index in the paragraph text to replace.
179
   * @param posEnded The ending index in the paragraph text to replace.
180
   * @param text     Overwrite the paragraph substring with this text.
181
   */
182
  private void replaceText(
183
      final int posBegan, final int posEnded, final String text ) {
184
    final int p = getCurrentParagraph();
185
186
    getEditor().replaceText( p, posBegan, p, posEnded, text );
187
  }
188
189
  /**
190
   * Returns the caret's current paragraph position.
191
   *
192
   * @return A number greater than or equal to 0.
193
   */
194
  private int getCurrentParagraph() {
195
    return getEditor().getCurrentParagraph();
196
  }
197
198
  /**
199
   * Returns the text for the paragraph that contains the caret.
200
   *
201
   * @return A non-null string, possibly empty.
202
   */
203
  private String getCaretParagraph() {
204
    return getEditor().getText( getCurrentParagraph() );
205
  }
206
207
  /**
208
   * Returns the caret position within the current paragraph.
209
   *
210
   * @return A value from 0 to the length of the current paragraph.
211
   */
212
  private int getCurrentCaretColumn() {
213
    return getEditor().getCaretColumn();
214
  }
215
216
  private VariableTreeItem<String> findLeaf( final String word ) {
217
    assert word != null;
218
219
    VariableTreeItem<String> leaf = findLeafExact( word );
220
221
    leaf = leaf == null ? findLeafStartsWith( word ) : leaf;
222
    leaf = leaf == null ? findLeafContains( word ) : leaf;
223
    leaf = leaf == null ? findLeafLevenshtein( word ) : leaf;
224
225
    return leaf;
226
  }
227
228
  private VariableTreeItem<String> findLeafExact( final String text ) {
229
    return findLeaf( text, EXACT );
230
  }
231
232
  private VariableTreeItem<String> findLeafContains( final String text ) {
233
    return findLeaf( text, CONTAINS );
234
  }
235
236
  private VariableTreeItem<String> findLeafStartsWith( final String text ) {
237
    return findLeaf( text, STARTS_WITH );
238
  }
239
240
  private VariableTreeItem<String> findLeafLevenshtein( final String text ) {
241
    return findLeaf( text, LEVENSHTEIN );
242
  }
243
244
  /**
245
   * Finds the first leaf having a value that starts with the given text, or
246
   * contains the text if contains is true.
247
   *
248
   * @param text     The text to find in the definition tree.
249
   * @param findMode Dictates what search criteria to use for matching words.
250
   * @return The leaf that starts with the given text, or null if not found.
251
   */
252
  private VariableTreeItem<String> findLeaf(
253
      final String text, final FindMode findMode ) {
254
    return getDefinitionPane().findLeaf( text, findMode );
255
  }
256
257
  /**
258
   * Collapses the tree then expands and selects the given node.
259
   *
260
   * @param node The node to expand.
261
   */
262
  private void expand( final TreeItem<String> node ) {
263
    final DefinitionPane pane = getDefinitionPane();
264
    pane.collapse();
265
    pane.expand( node );
266
    pane.select( node );
267
  }
268
269
  /**
270
   * @return A variable decorator that corresponds to the given file type.
271
   */
272
  private VariableDecorator getVariableDecorator() {
273
    return VariableNameDecoratorFactory.newInstance( getFilename() );
274
  }
275
276
  private Path getFilename() {
277
    return getFileEditorTab().getPath();
278
  }
279
280
  private EditorPane getEditorPane() {
281
    return getFileEditorTab().getEditorPane();
282
  }
283
284
  /**
285
   * Delegates to the file editor pane, and, ultimately, to its text area.
286
   */
287
  private <T extends Event, U extends T> void addKeyboardListener(
288
      final EventPattern<? super T, ? extends U> event,
289
      final Consumer<? super U> consumer ) {
290
    getEditorPane().addKeyboardListener( event, consumer );
291
  }
292
293
  private StyledTextArea<?, ?> getEditor() {
294
    return getEditorPane().getEditor();
295
  }
296
297
  public FileEditorTab getFileEditorTab() {
298
    return mTab;
299
  }
300
301
  public void setFileEditorTab( final FileEditorTab tab ) {
302
    mTab = tab;
303
  }
304
305
  private DefinitionPane getDefinitionPane() {
306
    return mDefinitionPane;
307
  }
308
309
  private void setDefinitionPane( final DefinitionPane definitionPane ) {
310
    mDefinitionPane = definitionPane;
311
  }
312
}
1313
A src/main/java/com/scrivenvar/editors/markdown/HyperlinkModel.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
32
/**
33
 * Represents the model for a hyperlink: text and url text.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public class HyperlinkModel {
38
39
  private String text;
40
  private String url;
41
  private String title;
42
43
  /**
44
   * Constructs a new hyperlink model in Markdown format by default with no
45
   * title (i.e., tooltip).
46
   *
47
   * @param text The hyperlink text displayed (e.g., displayed to the user).
48
   * @param url The destination URL (e.g., when clicked).
49
   */
50
  public HyperlinkModel( final String text, final String url ) {
51
    this( text, url, null );
52
  }
53
54
  /**
55
   * Constructs a new hyperlink model for the given AST link.
56
   * 
57
   * @param link A markdown link.
58
   */
59
  public HyperlinkModel( final Link link ) {
60
    this(
61
      link.getText().toString(),
62
      link.getUrl().toString(),
63
      link.getTitle().toString()
64
    );
65
  }
66
67
  /**
68
   * Constructs a new hyperlink model in Markdown format by default.
69
   *
70
   * @param text The hyperlink text displayed (e.g., displayed to the user).
71
   * @param url The destination URL (e.g., when clicked).
72
   * @param title The hyperlink title (e.g., shown as a tooltip).
73
   */
74
  public HyperlinkModel( final String text, final String url, final String title ) {
75
    setText( text );
76
    setUrl( url );
77
    setTitle( title );
78
  }
79
80
  /**
81
   * Returns the string in Markdown format by default.
82
   *
83
   * @return A markdown version of the hyperlink.
84
   */
85
  @Override
86
  public String toString() {
87
    String format = "%s%s%s";
88
89
    if( hasText() ) {
90
      format = "[%s]" + (hasTitle() ? "(%s \"%s\")" : "(%s%s)");
91
    }
92
93
    // Becomes ""+URL+"" if no text is set.
94
    // Becomes [TITLE]+(URL)+"" if no title is set.
95
    // Becomes [TITLE]+(URL+ \"TITLE\") if title is set.
96
    return String.format( format, getText(), getUrl(), getTitle() );
97
  }
98
99
  public final void setText( final String text ) {
100
    this.text = nullSafe( text );
101
  }
102
103
  public final void setUrl( final String url ) {
104
    this.url = nullSafe( url );
105
  }
106
107
  public final void setTitle( final String title ) {
108
    this.title = nullSafe( title );
109
  }
110
111
  /**
112
   * Answers whether text has been set for the hyperlink.
113
   *
114
   * @return true This is a text link.
115
   */
116
  public boolean hasText() {
117
    return !getText().isEmpty();
118
  }
119
120
  /**
121
   * Answers whether a title (tooltip) has been set for the hyperlink.
122
   *
123
   * @return true There is a title.
124
   */
125
  public boolean hasTitle() {
126
    return !getTitle().isEmpty();
127
  }
128
129
  public String getText() {
130
    return this.text;
131
  }
132
133
  public String getUrl() {
134
    return this.url;
135
  }
136
137
  public String getTitle() {
138
    return this.title;
139
  }
140
141
  private String nullSafe( final String s ) {
142
    return s == null ? "" : s;
143
  }
144
}
1145
A src/main/java/com/scrivenvar/editors/markdown/LinkVisitor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.vladsch.flexmark.ast.Link;
31
import com.vladsch.flexmark.util.ast.Node;
32
import com.vladsch.flexmark.util.ast.NodeVisitor;
33
import com.vladsch.flexmark.util.ast.VisitHandler;
34
35
/**
36
 * @author White Magic Software, Ltd.
37
 */
38
public class LinkVisitor {
39
40
  private NodeVisitor visitor;
41
  private Link link;
42
  private final int offset;
43
44
  /**
45
   * Creates a hyperlink given an offset into a paragraph and the markdown AST
46
   * link node.
47
   *
48
   * @param index Index into the paragraph that indicates the hyperlink to
49
   *              change.
50
   */
51
  public LinkVisitor( final int index ) {
52
    this.offset = index;
53
  }
54
55
  public Link process( final Node root ) {
56
    getVisitor().visit( root );
57
    return getLink();
58
  }
59
60
  /**
61
   * @param link Not null.
62
   */
63
  private void visit( final Link link ) {
64
    final int began = link.getStartOffset();
65
    final int ended = link.getEndOffset();
66
    final int index = getOffset();
67
68
    if( index >= began && index <= ended ) {
69
      setLink( link );
70
    }
71
  }
72
73
  private synchronized NodeVisitor getVisitor() {
74
    if( this.visitor == null ) {
75
      this.visitor = createVisitor();
76
    }
77
78
    return this.visitor;
79
  }
80
81
  protected NodeVisitor createVisitor() {
82
    return new NodeVisitor(
83
        new VisitHandler<>( Link.class, LinkVisitor.this::visit ) );
84
  }
85
86
  private Link getLink() {
87
    return this.link;
88
  }
89
90
  private void setLink( final Link link ) {
91
    this.link = link;
92
  }
93
94
  public int getOffset() {
95
    return this.offset;
96
  }
97
}
198
A src/main/java/com/scrivenvar/editors/markdown/MarkdownEditorPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.editors.markdown;
29
30
import com.scrivenvar.dialogs.ImageDialog;
31
import com.scrivenvar.dialogs.LinkDialog;
32
import com.scrivenvar.editors.EditorPane;
33
import com.scrivenvar.processors.markdown.MarkdownProcessor;
34
import com.vladsch.flexmark.ast.Link;
35
import com.vladsch.flexmark.util.ast.Node;
36
import javafx.scene.control.Dialog;
37
import javafx.scene.control.IndexRange;
38
import javafx.scene.input.KeyEvent;
39
import javafx.stage.Window;
40
import org.fxmisc.richtext.StyleClassedTextArea;
41
42
import java.nio.file.Path;
43
import java.util.regex.Matcher;
44
import java.util.regex.Pattern;
45
46
import static com.scrivenvar.Constants.STYLESHEET_MARKDOWN;
47
import static com.scrivenvar.util.Utils.ltrim;
48
import static com.scrivenvar.util.Utils.rtrim;
49
import static javafx.scene.input.KeyCode.ENTER;
50
import static org.fxmisc.wellbehaved.event.EventPattern.keyPressed;
51
52
/**
53
 * Markdown editor pane.
54
 *
55
 * @author Karl Tauber and White Magic Software, Ltd.
56
 */
57
public class MarkdownEditorPane extends EditorPane {
58
59
  private static final Pattern AUTO_INDENT_PATTERN = Pattern.compile(
60
      "(\\s*[*+-]\\s+|\\s*[0-9]+\\.\\s+|\\s+)(.*)" );
61
62
  public MarkdownEditorPane() {
63
    initEditor();
64
  }
65
66
  private void initEditor() {
67
    final StyleClassedTextArea textArea = getEditor();
68
69
    textArea.setWrapText( true );
70
    textArea.getStyleClass().add( "markdown-editor" );
71
    textArea.getStylesheets().add( STYLESHEET_MARKDOWN );
72
73
    addKeyboardListener( keyPressed( ENTER ), this::enterPressed );
74
  }
75
76
  private void enterPressed( final KeyEvent e ) {
77
    final StyleClassedTextArea textArea = getEditor();
78
    final String currentLine =
79
        textArea.getText( textArea.getCurrentParagraph() );
80
    final Matcher matcher = AUTO_INDENT_PATTERN.matcher( currentLine );
81
82
    String newText = "\n";
83
84
    if( matcher.matches() ) {
85
      if( !matcher.group( 2 ).isEmpty() ) {
86
        // indent new line with same whitespace characters and list markers
87
        // as current line
88
        newText = newText.concat( matcher.group( 1 ) );
89
      }
90
      else {
91
        // current line contains only whitespace characters and list markers
92
        // --> empty current line
93
        final int caretPosition = textArea.getCaretPosition();
94
        textArea.selectRange( caretPosition - currentLine.length(),
95
                              caretPosition );
96
      }
97
    }
98
99
    textArea.replaceSelection( newText );
100
101
    // Ensure that the window scrolls when Enter is pressed at the bottom of
102
    // the pane.
103
    textArea.requestFollowCaret();
104
  }
105
106
  public void surroundSelection( final String leading, final String trailing ) {
107
    surroundSelection( leading, trailing, null );
108
  }
109
110
  public void surroundSelection( String leading, String trailing,
111
                                 final String hint ) {
112
    final StyleClassedTextArea textArea = getEditor();
113
114
    // Note: not using textArea.insertText() to insert leading and trailing
115
    // because this would add two changes to undo history
116
    final IndexRange selection = textArea.getSelection();
117
    int start = selection.getStart();
118
    int end = selection.getEnd();
119
120
    final String selectedText = textArea.getSelectedText();
121
122
    // remove leading and trailing whitespaces from selected text
123
    String trimmedSelectedText = selectedText.trim();
124
    if( trimmedSelectedText.length() < selectedText.length() ) {
125
      start += selectedText.indexOf( trimmedSelectedText );
126
      end = start + trimmedSelectedText.length();
127
    }
128
129
    // remove leading whitespaces from leading text if selection starts at zero
130
    if( start == 0 ) {
131
      leading = ltrim( leading );
132
    }
133
134
    // remove trailing whitespaces from trailing text if selection ends at
135
    // text end
136
    if( end == textArea.getLength() ) {
137
      trailing = rtrim( trailing );
138
    }
139
140
    // remove leading line separators from leading text
141
    // if there are line separators before the selected text
142
    if( leading.startsWith( "\n" ) ) {
143
      for( int i = start - 1; i >= 0 && leading.startsWith( "\n" ); i-- ) {
144
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
145
          break;
146
        }
147
        leading = leading.substring( 1 );
148
      }
149
    }
150
151
    // remove trailing line separators from trailing or leading text
152
    // if there are line separators after the selected text
153
    final boolean trailingIsEmpty = trailing.isEmpty();
154
    String str = trailingIsEmpty ? leading : trailing;
155
156
    if( str.endsWith( "\n" ) ) {
157
      final int length = textArea.getLength();
158
159
      for( int i = end; i < length && str.endsWith( "\n" ); i++ ) {
160
        if( !"\n".equals( textArea.getText( i, i + 1 ) ) ) {
161
          break;
162
        }
163
164
        str = str.substring( 0, str.length() - 1 );
165
      }
166
167
      if( trailingIsEmpty ) {
168
        leading = str;
169
      }
170
      else {
171
        trailing = str;
172
      }
173
    }
174
175
    int selStart = start + leading.length();
176
    int selEnd = end + leading.length();
177
178
    // insert hint text if selection is empty
179
    if( hint != null && trimmedSelectedText.isEmpty() ) {
180
      trimmedSelectedText = hint;
181
      selEnd = selStart + hint.length();
182
    }
183
184
    // prevent undo merging with previous text entered by user
185
    getUndoManager().preventMerge();
186
187
    // replace text and update selection
188
    textArea.replaceText( start,
189
                          end,
190
                          leading + trimmedSelectedText + trailing );
191
    textArea.selectRange( selStart, selEnd );
192
  }
193
194
  /**
195
   * Returns one of: selected text, word under cursor, or parsed hyperlink from
196
   * the markdown AST.
197
   *
198
   * @return An instance containing the link URL and display text.
199
   */
200
  private HyperlinkModel getHyperlink() {
201
    final StyleClassedTextArea textArea = getEditor();
202
    final String selectedText = textArea.getSelectedText();
203
204
    // Get the current paragraph, convert to Markdown nodes.
205
    final MarkdownProcessor mp = new MarkdownProcessor( null );
206
    final int p = textArea.getCurrentParagraph();
207
    final String paragraph = textArea.getText( p );
208
    final Node node = mp.toNode( paragraph );
209
    final LinkVisitor visitor = new LinkVisitor( textArea.getCaretColumn() );
210
    final Link link = visitor.process( node );
211
212
    if( link != null ) {
213
      textArea.selectRange( p, link.getStartOffset(), p, link.getEndOffset() );
214
    }
215
216
    return createHyperlinkModel(
217
        link, selectedText, "https://localhost"
218
    );
219
  }
220
221
  @SuppressWarnings("SameParameterValue")
222
  private HyperlinkModel createHyperlinkModel(
223
      final Link link, final String selection, final String url ) {
224
225
    return link == null
226
        ? new HyperlinkModel( selection, url )
227
        : new HyperlinkModel( link );
228
  }
229
230
  private Path getParentPath() {
231
    final Path path = getPath();
232
    return (path != null) ? path.getParent() : null;
233
  }
234
235
  private Dialog<String> createLinkDialog() {
236
    return new LinkDialog( getWindow(), getHyperlink() );
237
  }
238
239
  private Dialog<String> createImageDialog() {
240
    return new ImageDialog( getWindow(), getParentPath() );
241
  }
242
243
  private void insertObject( final Dialog<String> dialog ) {
244
    dialog.showAndWait().ifPresent(
245
        result -> getEditor().replaceSelection( result )
246
    );
247
  }
248
249
  public void insertLink() {
250
    insertObject( createLinkDialog() );
251
  }
252
253
  public void insertImage() {
254
    insertObject( createImageDialog() );
255
  }
256
257
  private Window getWindow() {
258
    return getScrollPane().getScene().getWindow();
259
  }
260
}
1261
A src/main/java/com/scrivenvar/predicates/files/FileTypePredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.files;
29
30
import java.io.File;
31
import java.nio.file.FileSystems;
32
import java.nio.file.PathMatcher;
33
import java.util.Collection;
34
import java.util.function.Predicate;
35
36
/**
37
 * Responsible for testing whether a given path (to a file) matches one of the
38
 * filename extension patterns provided during construction.
39
 *
40
 * @author White Magic Software, Ltd.
41
 */
42
public class FileTypePredicate implements Predicate<File> {
43
44
  private final PathMatcher mMatcher;
45
46
  /**
47
   * Constructs a new instance given a set of file extension globs.
48
   *
49
   * @param patterns Comma-separated list of globbed extensions including the
50
   * Kleene star (e.g., <code>*.md,*.markdown,*.txt</code>).
51
   */
52
  public FileTypePredicate( final String patterns ) {
53
    mMatcher = FileSystems.getDefault().getPathMatcher(
54
      "glob:**{" + patterns + "}"
55
    );
56
  }
57
58
  /**
59
   * Constructs a new instance given a list of file extension globs, each must
60
   * include the Kleene star (a.k.a. asterisk).
61
   *
62
   * @param patterns Collection of globbed extensions.
63
   */
64
  public FileTypePredicate( final Collection<String> patterns ) {
65
    this( String.join( ",", patterns ) );
66
  }
67
68
  /**
69
   * Returns true if the file matches the patterns defined during construction.
70
   *
71
   * @param file The filename to match against the given glob patterns.
72
   *
73
   * @return false The filename does not match the glob patterns.
74
   */
75
  @Override
76
  public boolean test( final File file ) {
77
    return getMatcher().matches( file.toPath() );
78
  }
79
80
  private PathMatcher getMatcher() {
81
    return mMatcher;
82
  }
83
}
184
A src/main/java/com/scrivenvar/predicates/strings/ContainsPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
/**
31
 * Determines if one string contains another.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class ContainsPredicate extends StringPredicate {
36
37
  /**
38
   * Calls the superclass to construct the instance.
39
   *
40
   * @param comparate Not null.
41
   */
42
  public ContainsPredicate( final String comparate ) {
43
    super( comparate );
44
  }
45
46
  /**
47
   * Answers whether the given strings match each other. What match means will
48
   * depend on user preferences. The empty condition is required to return the
49
   * first node in a list of child nodes when the user has not yet selected a
50
   * node.
51
   *
52
   * @param comparator The string to compare against the comparate.
53
   *
54
   * @return true if s1 and s2 are a match according to some criteria,or s2 is
55
   * empty.
56
   */
57
  @Override
58
  public boolean test( final String comparator ) {
59
    final String comparate = getComparate().toLowerCase();
60
    return comparator.contains( comparate.toLowerCase() )
61
      || comparate.isEmpty();
62
  }
63
}
164
A src/main/java/com/scrivenvar/predicates/strings/StartsPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
/**
31
 * Determines if a string starts with another.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public class StartsPredicate extends StringPredicate {
36
37
  /**
38
   * Constructs a new instance using a comparate that will be compared with
39
   * the comparator during the test.
40
   *
41
   * @param comparate The string to compare against the comparator.
42
   */
43
  public StartsPredicate( final String comparate ) {
44
    super( comparate );
45
  }
46
47
  /**
48
   * Compares two strings.
49
   *
50
   * @param comparator A non-null string, possibly empty.
51
   *
52
   * @return true The comparator starts with the comparate, ignoring case.
53
   */
54
  @Override
55
  public boolean test( final String comparator ) {
56
    final String comparate = getComparate().toLowerCase();
57
    return comparator.startsWith( comparate.toLowerCase() );
58
  }
59
}
160
A src/main/java/com/scrivenvar/predicates/strings/StringPredicate.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.predicates.strings;
29
30
import java.util.function.Predicate;
31
32
/**
33
 * General predicate for different types of string comparisons.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public abstract class StringPredicate implements Predicate<String> {
38
39
  private final String comparate;
40
41
  public StringPredicate( final String comparate ) {
42
    this.comparate = comparate;
43
  }
44
45
  protected String getComparate() {
46
    return this.comparate;
47
  }
48
}
149
A src/main/java/com/scrivenvar/preferences/FilePreferences.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.events.Notifier;
32
33
import java.io.File;
34
import java.io.FileInputStream;
35
import java.io.FileOutputStream;
36
import java.util.*;
37
import java.util.prefs.AbstractPreferences;
38
import java.util.prefs.BackingStoreException;
39
40
/**
41
 * Preferences implementation that stores to a user-defined file. Local file
42
 * storage is preferred over a certain operating system's monolithic trash heap
43
 * called a registry. When the OS is locked down, the default Preferences
44
 * implementation will try to write to the registry and fail due to permissions
45
 * problems. This class sidesteps the issue entirely by writing to the user's
46
 * home directory, where permissions should be a bit more lax.
47
 */
48
public class FilePreferences extends AbstractPreferences {
49
  private final Notifier mNotifier = Services.load( Notifier.class );
50
51
  private final Map<String, String> mRoot = new TreeMap<>();
52
  private final Map<String, FilePreferences> mChildren = new TreeMap<>();
53
  private boolean mRemoved;
54
55
  private final Object mMutex = new Object();
56
57
  public FilePreferences(
58
      final AbstractPreferences parent, final String name ) {
59
    super( parent, name );
60
61
    try {
62
      sync();
63
    } catch( final BackingStoreException ex ) {
64
      error( ex );
65
    }
66
  }
67
68
  @Override
69
  protected void putSpi( final String key, final String value ) {
70
    mRoot.put( key, value );
71
72
    try {
73
      flush();
74
    } catch( final BackingStoreException ex ) {
75
      error( ex );
76
    }
77
  }
78
79
  @Override
80
  protected String getSpi( final String key ) {
81
    return mRoot.get( key );
82
  }
83
84
  @Override
85
  protected void removeSpi( final String key ) {
86
    mRoot.remove( key );
87
88
    try {
89
      flush();
90
    } catch( final BackingStoreException ex ) {
91
      error( ex );
92
    }
93
  }
94
95
  @Override
96
  protected void removeNodeSpi() throws BackingStoreException {
97
    mRemoved = true;
98
    flush();
99
  }
100
101
  @Override
102
  protected String[] keysSpi() {
103
    return mRoot.keySet().toArray( new String[ 0 ] );
104
  }
105
106
  @Override
107
  protected String[] childrenNamesSpi() {
108
    return mChildren.keySet().toArray( new String[ 0 ] );
109
  }
110
111
  @Override
112
  protected FilePreferences childSpi( final String name ) {
113
    FilePreferences child = mChildren.get( name );
114
115
    if( child == null || child.isRemoved() ) {
116
      child = new FilePreferences( this, name );
117
      mChildren.put( name, child );
118
    }
119
120
    return child;
121
  }
122
123
  @Override
124
  protected void syncSpi() {
125
    if( isRemoved() ) {
126
      return;
127
    }
128
129
    final File file = FilePreferencesFactory.getPreferencesFile();
130
131
    if( !file.exists() ) {
132
      return;
133
    }
134
135
    synchronized( mMutex ) {
136
      final Properties p = new Properties();
137
138
      try {
139
        p.load( new FileInputStream( file ) );
140
141
        final String path = getPath();
142
        final Enumeration<?> propertyNames = p.propertyNames();
143
144
        while( propertyNames.hasMoreElements() ) {
145
          final String propKey = (String) propertyNames.nextElement();
146
147
          if( propKey.startsWith( path ) ) {
148
            final String subKey = propKey.substring( path.length() );
149
150
            // Only load immediate descendants
151
            if( subKey.indexOf( '.' ) == -1 ) {
152
              mRoot.put( subKey, p.getProperty( propKey ) );
153
            }
154
          }
155
        }
156
      } catch( final Exception ex ) {
157
        error( new BackingStoreException( ex ) );
158
      }
159
    }
160
  }
161
162
  private String getPath() {
163
    final FilePreferences parent = (FilePreferences) parent();
164
165
    return parent == null ? "" : parent.getPath() + name() + '.';
166
  }
167
168
  @Override
169
  protected void flushSpi() {
170
    final File file = FilePreferencesFactory.getPreferencesFile();
171
172
    synchronized( mMutex ) {
173
      final Properties p = new Properties();
174
175
      try {
176
        final String path = getPath();
177
178
        if( file.exists() ) {
179
          p.load( new FileInputStream( file ) );
180
181
          final List<String> toRemove = new ArrayList<>();
182
183
          // Make a list of all direct children of this node to be removed
184
          final Enumeration<?> propertyNames = p.propertyNames();
185
186
          while( propertyNames.hasMoreElements() ) {
187
            final String propKey = (String) propertyNames.nextElement();
188
            if( propKey.startsWith( path ) ) {
189
              final String subKey = propKey.substring( path.length() );
190
191
              // Only do immediate descendants
192
              if( subKey.indexOf( '.' ) == -1 ) {
193
                toRemove.add( propKey );
194
              }
195
            }
196
          }
197
198
          // Remove them now that the enumeration is done with
199
          for( final String propKey : toRemove ) {
200
            p.remove( propKey );
201
          }
202
        }
203
204
        // If this node hasn't been removed, add back in any values
205
        if( !mRemoved ) {
206
          for( final String s : mRoot.keySet() ) {
207
            p.setProperty( path + s, mRoot.get( s ) );
208
          }
209
        }
210
211
        p.store( new FileOutputStream( file ), "FilePreferences" );
212
      } catch( final Exception ex ) {
213
        error( new BackingStoreException( ex ) );
214
      }
215
    }
216
  }
217
218
  private void error( final BackingStoreException ex ) {
219
    getNotifier().notify( ex );
220
  }
221
222
  private Notifier getNotifier() {
223
    return mNotifier;
224
  }
225
}
1226
A src/main/java/com/scrivenvar/preferences/FilePreferencesFactory.java
1
/*
2
 * Copyright 2016 David Croft and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import java.io.File;
31
import java.nio.file.FileSystems;
32
import java.util.prefs.Preferences;
33
import java.util.prefs.PreferencesFactory;
34
35
import static com.scrivenvar.Constants.APP_TITLE;
36
37
/**
38
 * PreferencesFactory implementation that stores the preferences in a
39
 * user-defined file. Usage:
40
 * <pre>
41
 * System.setProperty( "java.util.prefs.PreferencesFactory",
42
 * FilePreferencesFactory.class.getName() );
43
 * </pre>
44
 * <p>
45
 * The file defaults to <code>$user.home/.scrivenvar</code>, but can be changed
46
 * using <code>-Dapplication.name=preferences</code> when running the
47
 * application, or by calling <code>System.setProperty</code> with the
48
 * "application.name" property.
49
 * </p>
50
 */
51
public class FilePreferencesFactory implements PreferencesFactory {
52
53
  private static File preferencesFile;
54
  private Preferences rootPreferences;
55
56
  @Override
57
  public Preferences systemRoot() {
58
    return userRoot();
59
  }
60
61
  @Override
62
  public synchronized Preferences userRoot() {
63
    if( rootPreferences == null ) {
64
      rootPreferences = new FilePreferences( null, "" );
65
    }
66
67
    return rootPreferences;
68
  }
69
70
  public synchronized static File getPreferencesFile() {
71
    if( preferencesFile == null ) {
72
      String prefsFile = getPreferencesFilename();
73
74
      preferencesFile = new File( prefsFile ).getAbsoluteFile();
75
    }
76
77
    return preferencesFile;
78
  }
79
80
  public static String getPreferencesFilename() {
81
    final String filename = System.getProperty( "application.name", APP_TITLE );
82
    return System.getProperty( "user.home" ) + getSeparator() + "." + filename;
83
  }
84
85
  public static String getSeparator() {
86
    return FileSystems.getDefault().getSeparator();
87
  }
88
}
189
A src/main/java/com/scrivenvar/preferences/UserPreferences.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preferences;
29
30
import com.dlsc.formsfx.model.structure.StringField;
31
import com.dlsc.preferencesfx.PreferencesFx;
32
import com.dlsc.preferencesfx.model.Category;
33
import com.dlsc.preferencesfx.model.Group;
34
import com.dlsc.preferencesfx.model.Setting;
35
import com.scrivenvar.Services;
36
import com.scrivenvar.service.Settings;
37
import javafx.beans.property.ObjectProperty;
38
import javafx.beans.property.SimpleObjectProperty;
39
import javafx.beans.property.SimpleStringProperty;
40
import javafx.beans.property.StringProperty;
41
import javafx.scene.Node;
42
import javafx.scene.control.Label;
43
44
import java.io.File;
45
import java.nio.file.Path;
46
47
import static com.scrivenvar.Constants.PERSIST_IMAGES_DEFAULT;
48
import static com.scrivenvar.Constants.USER_DIRECTORY;
49
import static com.scrivenvar.Messages.get;
50
51
public class UserPreferences {
52
  private final Settings SETTINGS = Services.load( Settings.class );
53
54
  private final ObjectProperty<File> mPropRDirectory;
55
  private final StringProperty mPropRScript;
56
  private final ObjectProperty<File> mPropImagesDirectory;
57
  private final StringProperty mPropImagesOrder;
58
  private final ObjectProperty<File> mPropDefinitionPath;
59
60
  private final PreferencesFx mPreferencesFx;
61
62
  public UserPreferences() {
63
    mPropRDirectory = simpleFile( USER_DIRECTORY );
64
    mPropRScript = new SimpleStringProperty( "" );
65
66
    mPropImagesDirectory = simpleFile( USER_DIRECTORY );
67
    mPropImagesOrder = new SimpleStringProperty( PERSIST_IMAGES_DEFAULT );
68
69
    mPropDefinitionPath = simpleFile( getSetting(
70
        "file.definition.default", "variables.yaml" )
71
    );
72
73
    mPreferencesFx = createPreferencesFx();
74
  }
75
76
  /**
77
   * Display the user preferences settings dialog (non-modal).
78
   */
79
  public void show() {
80
    mPreferencesFx.show( false );
81
  }
82
83
  /**
84
   * Call to persist the settings. Strictly speaking, this could watch on
85
   * all values for external changes then save automatically.
86
   */
87
  public void save() {
88
    mPreferencesFx.saveSettings();
89
  }
90
91
  /**
92
   * Creates the preferences dialog.
93
   * <p>
94
   * TODO: Make this dynamic by iterating over all "Preferences.*" values
95
   * that follow a particular naming pattern.
96
   * </p>
97
   *
98
   * @return A new instance of preferences for users to edit.
99
   */
100
  @SuppressWarnings("unchecked")
101
  private PreferencesFx createPreferencesFx() {
102
    final Setting<StringField, StringProperty> scriptSetting =
103
        Setting.of( "Script", mPropRScript );
104
    final StringField field = scriptSetting.getElement();
105
    field.multiline( true );
106
107
    return PreferencesFx.of(
108
        UserPreferences.class,
109
        Category.of(
110
            get( "Preferences.r" ),
111
            Group.of(
112
                get( "Preferences.r.directory" ),
113
                Setting.of( label( "Preferences.r.directory.desc", false ) ),
114
                Setting.of( "Directory", mPropRDirectory, true )
115
            ),
116
            Group.of(
117
                get( "Preferences.r.script" ),
118
                Setting.of( label( "Preferences.r.script.desc" ) ),
119
                scriptSetting
120
            )
121
        ),
122
        Category.of(
123
            get( "Preferences.images" ),
124
            Group.of(
125
                get( "Preferences.images.directory" ),
126
                Setting.of( label( "Preferences.images.directory.desc" ) ),
127
                Setting.of( "Directory", mPropImagesDirectory, true )
128
            ),
129
            Group.of(
130
                get( "Preferences.images.suffixes" ),
131
                Setting.of( label( "Preferences.images.suffixes.desc" ) ),
132
                Setting.of( "Extensions", mPropImagesOrder )
133
            )
134
        ),
135
        Category.of(
136
            get( "Preferences.definitions" ),
137
            Group.of(
138
                get( "Preferences.definitions.path" ),
139
                Setting.of( label( "Preferences.definitions.path.desc" ) ),
140
                Setting.of( "Path", mPropDefinitionPath, false )
141
            )
142
        )
143
    );
144
  }
145
146
  /**
147
   * Wraps a {@link File} inside a {@link SimpleObjectProperty}.
148
   *
149
   * @param path The file name to use when constructing the {@link File}.
150
   * @return A new {@link SimpleObjectProperty} instance with a {@link File}
151
   * that references the given {@code path}.
152
   */
153
  private SimpleObjectProperty<File> simpleFile( final String path ) {
154
    return new SimpleObjectProperty<>( new File( path ) );
155
  }
156
157
  /**
158
   * Creates a label for the given key after interpolating its value.
159
   *
160
   * @param key The key to find in the resource bundle.
161
   * @return The value of the key as a label.
162
   */
163
  private Node label( final String key ) {
164
    return new Label( get( key, true ) );
165
  }
166
167
  /**
168
   * Creates a label for the given key.
169
   *
170
   * @param key         The key to find in the resource bundle.
171
   * @param interpolate {@code true} means to interpolate the value.
172
   * @return The value of the key, interpolated if {@code interpolate} is
173
   * {@code true}.
174
   */
175
  @SuppressWarnings("SameParameterValue")
176
  private Node label( final String key, final boolean interpolate ) {
177
    return new Label( get( key, interpolate ) );
178
  }
179
180
  /**
181
   * Returns the value for a key from the settings properties file.
182
   *
183
   * @param key   Key within the settings properties file to find.
184
   * @param value Default value to return if the key is not found.
185
   * @return The value for the given key from the settings file, or the
186
   * given {@code value} if no key found.
187
   */
188
  @SuppressWarnings("SameParameterValue")
189
  private String getSetting( final String key, final String value ) {
190
    return SETTINGS.getSetting( key, value );
191
  }
192
193
  public ObjectProperty<File> definitionPathProperty() {
194
    return mPropDefinitionPath;
195
  }
196
197
  public Path getDefinitionPath() {
198
    return definitionPathProperty().getValue().toPath();
199
  }
200
201
  private ObjectProperty<File> rDirectoryProperty() {
202
    return mPropRDirectory;
203
  }
204
205
  public File getRDirectory() {
206
    return rDirectoryProperty().getValue();
207
  }
208
209
  private StringProperty rScriptProperty() {
210
    return mPropRScript;
211
  }
212
213
  public String getRScript() {
214
    return rScriptProperty().getValue();
215
  }
216
217
  private ObjectProperty<File> imagesDirectoryProperty() {
218
    return mPropImagesDirectory;
219
  }
220
221
  public File getImagesDirectory() {
222
    return imagesDirectoryProperty().getValue();
223
  }
224
225
  private StringProperty imagesOrderProperty() {
226
    return mPropImagesOrder;
227
  }
228
229
  public String getImagesOrder() {
230
    return imagesOrderProperty().getValue();
231
  }
232
}
1233
A src/main/java/com/scrivenvar/preview/ChainedReplacedElementFactory.java
1
/*
2
 * {{{ header & license
3
 * Copyright (c) 2006 Patrick Wright
4
 * Copyright (c) 2007 Wisconsin Court System
5
 *
6
 * This program is free software; you can redistribute it and/or
7
 * modify it under the terms of the GNU Lesser General Public License
8
 * as published by the Free Software Foundation; either version 2.1
9
 * of the License, or (at your option) any later version.
10
 *
11
 * This program is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.	See the
14
 * GNU Lesser General Public License for more details.
15
 *
16
 * You should have received a copy of the GNU Lesser General Public License
17
 * along with this program; if not, write to the Free Software
18
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
19
 * }}}
20
 */
21
package com.scrivenvar.preview;
22
23
import org.w3c.dom.Element;
24
import org.xhtmlrenderer.extend.ReplacedElement;
25
import org.xhtmlrenderer.extend.ReplacedElementFactory;
26
import org.xhtmlrenderer.extend.UserAgentCallback;
27
import org.xhtmlrenderer.layout.LayoutContext;
28
import org.xhtmlrenderer.render.BlockBox;
29
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
30
31
import java.util.ArrayList;
32
import java.util.List;
33
34
public class ChainedReplacedElementFactory implements ReplacedElementFactory {
35
  private final List<ReplacedElementFactory> mFactoryList = new ArrayList<>();
36
37
  public ChainedReplacedElementFactory() {
38
  }
39
40
  public ReplacedElement createReplacedElement(
41
      LayoutContext c, BlockBox box, UserAgentCallback uac,
42
      int cssWidth, int cssHeight ) {
43
    ReplacedElement re = null;
44
45
    for( final ReplacedElementFactory ref : mFactoryList ) {
46
      re = ref.createReplacedElement( c, box, uac, cssWidth, cssHeight );
47
48
      if( re != null ) {
49
        break;
50
      }
51
    }
52
53
    return re;
54
  }
55
56
  public void addFactory( final ReplacedElementFactory factory ) {
57
    mFactoryList.add( factory );
58
  }
59
60
  public void reset() {
61
    for( final ReplacedElementFactory factory : mFactoryList ) {
62
      factory.reset();
63
    }
64
  }
65
66
  public void remove( final Element element ) {
67
    for( final ReplacedElementFactory factory : mFactoryList ) {
68
      factory.remove( element );
69
    }
70
  }
71
72
  public void setFormSubmissionListener( FormSubmissionListener listener ) {
73
  }
74
}
175
A src/main/java/com/scrivenvar/preview/HTMLPreviewPane.java
1
/*
2
 * Copyright 2016 Karl Tauber and White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import javafx.embed.swing.SwingNode;
31
import javafx.scene.Node;
32
import javafx.scene.layout.Pane;
33
import org.jsoup.Jsoup;
34
import org.jsoup.helper.W3CDom;
35
import org.jsoup.nodes.Document;
36
import org.xhtmlrenderer.simple.XHTMLPanel;
37
import org.xhtmlrenderer.simple.extend.XhtmlNamespaceHandler;
38
import org.xhtmlrenderer.swing.SwingReplacedElementFactory;
39
40
import javax.swing.*;
41
import java.nio.file.Path;
42
43
import static com.scrivenvar.Constants.STYLESHEET_PREVIEW;
44
45
/**
46
 * HTML preview pane is responsible for rendering an HTML document.
47
 *
48
 * @author Karl Tauber and White Magic Software, Ltd.
49
 */
50
public final class HTMLPreviewPane extends Pane {
51
  private static class HTMLPanel extends XHTMLPanel {
52
    /**
53
     * Prevent scrolling to the top.
54
     */
55
    @Override
56
    public void resetScrollPosition() {
57
    }
58
  }
59
60
  private final static String HTML_HEADER = "<!DOCTYPE html>"
61
      + "<html>"
62
      + "<head>"
63
      + "<link rel='stylesheet' href='" +
64
      HTMLPreviewPane.class.getResource( STYLESHEET_PREVIEW ) + "'/>"
65
      + "</head>"
66
      + "<body>";
67
  private final static String HTML_FOOTER = "</body></html>";
68
69
  private final StringBuilder mHtml = new StringBuilder( 65536 );
70
  private final int mHtmlPrefixLength;
71
72
  private final W3CDom mW3cDom = new W3CDom();
73
  private final XhtmlNamespaceHandler mNamespaceHandler =
74
      new XhtmlNamespaceHandler();
75
  private final HTMLPanel mRenderer = new HTMLPanel();
76
  private final SwingNode mSwingNode = new SwingNode();
77
  private final JScrollPane mScrollPane = new JScrollPane( mRenderer );
78
79
  private Path mPath;
80
81
  /**
82
   * Creates a new preview pane that can scroll to the caret position within the
83
   * document.
84
   */
85
  public HTMLPreviewPane() {
86
    final ChainedReplacedElementFactory factory =
87
        new ChainedReplacedElementFactory();
88
    factory.addFactory( new SVGReplacedElementFactory() );
89
    factory.addFactory( new SwingReplacedElementFactory() );
90
91
    mRenderer.getSharedContext().setReplacedElementFactory( factory );
92
    mRenderer.getSharedContext().getTextRenderer().setSmoothingThreshold( 0 );
93
    mSwingNode.setContent( mScrollPane );
94
95
    mHtml.append( HTML_HEADER );
96
    mHtmlPrefixLength = mHtml.length();
97
  }
98
99
  /**
100
   * Updates the internal HTML source, loads it into the preview pane, then
101
   * scrolls to the caret position.
102
   *
103
   * @param html The new HTML document to display.
104
   */
105
  public void update( final String html ) {
106
    final Document jsoupDoc = Jsoup.parse( decorate( html ) );
107
    org.w3c.dom.Document w3cDoc = mW3cDom.fromJsoup( jsoupDoc );
108
109
    mRenderer.setDocument( w3cDoc, getBaseUrl(), mNamespaceHandler );
110
  }
111
112
  private String decorate( final String html ) {
113
    mHtml.setLength( mHtmlPrefixLength );
114
    return mHtml.append( html )
115
                .append( HTML_FOOTER )
116
                .toString();
117
  }
118
119
  /**
120
   * Clears out the HTML content from the preview.
121
   */
122
  public void clear() {
123
    update( "" );
124
  }
125
126
  private String getBaseUrl() {
127
    final Path basePath = getPath();
128
    final Path parent = basePath == null ? null : basePath.getParent();
129
130
    return parent == null ? "" : parent.toUri().toString();
131
  }
132
133
  public Path getPath() {
134
    return mPath;
135
  }
136
137
  public void setPath( final Path path ) {
138
    assert path != null;
139
    mPath = path;
140
  }
141
142
  /**
143
   * Content to embed in a panel.
144
   *
145
   * @return The content to display to the user.
146
   */
147
  public Node getNode() {
148
    return mSwingNode;
149
  }
150
151
  public JScrollPane getScrollPane() {
152
    return mScrollPane;
153
  }
154
155
  public JScrollBar getVerticalScrollBar() {
156
    return getScrollPane().getVerticalScrollBar();
157
  }
158
}
1159
A src/main/java/com/scrivenvar/preview/SVGRasterizer.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import org.apache.batik.anim.dom.SAXSVGDocumentFactory;
31
import org.apache.batik.gvt.renderer.ImageRenderer;
32
import org.apache.batik.transcoder.TranscoderException;
33
import org.apache.batik.transcoder.TranscoderInput;
34
import org.apache.batik.transcoder.TranscoderOutput;
35
import org.apache.batik.transcoder.image.ImageTranscoder;
36
import org.w3c.dom.svg.SVGDocument;
37
38
import java.awt.*;
39
import java.awt.image.BufferedImage;
40
import java.io.IOException;
41
import java.net.URL;
42
import java.util.Map;
43
44
import static java.awt.Color.WHITE;
45
import static java.awt.RenderingHints.*;
46
import static org.apache.batik.transcoder.SVGAbstractTranscoder.KEY_WIDTH;
47
import static org.apache.batik.transcoder.image.ImageTranscoder.KEY_BACKGROUND_COLOR;
48
import static org.apache.batik.util.XMLResourceDescriptor.getXMLParserClassName;
49
50
public class SVGRasterizer {
51
  private final static SAXSVGDocumentFactory mFactory =
52
      new SAXSVGDocumentFactory( getXMLParserClassName() );
53
54
  private final static Map<Object, Object> RENDERING_HINTS = Map.of(
55
      KEY_ALPHA_INTERPOLATION,
56
      VALUE_ALPHA_INTERPOLATION_QUALITY,
57
      KEY_INTERPOLATION,
58
      VALUE_INTERPOLATION_BICUBIC,
59
      KEY_ANTIALIASING,
60
      VALUE_ANTIALIAS_ON,
61
      KEY_COLOR_RENDERING,
62
      VALUE_COLOR_RENDER_QUALITY,
63
      KEY_DITHERING,
64
      VALUE_DITHER_DISABLE,
65
      KEY_RENDERING,
66
      VALUE_RENDER_QUALITY,
67
      KEY_STROKE_CONTROL,
68
      VALUE_STROKE_PURE,
69
      KEY_FRACTIONALMETRICS,
70
      VALUE_FRACTIONALMETRICS_ON,
71
      KEY_TEXT_ANTIALIASING,
72
      VALUE_TEXT_ANTIALIAS_OFF
73
  );
74
75
  private static class BufferedImageTranscoder extends ImageTranscoder {
76
    private BufferedImage mImage;
77
78
    @Override
79
    public BufferedImage createImage( final int w, final int h ) {
80
      return new BufferedImage( w, h, BufferedImage.TYPE_INT_ARGB );
81
    }
82
83
    @Override
84
    public void writeImage(
85
        final BufferedImage image, final TranscoderOutput output ) {
86
      mImage = image;
87
    }
88
89
    public BufferedImage getBufferedImage() {
90
      return mImage;
91
    }
92
93
    @Override
94
    protected ImageRenderer createRenderer() {
95
      final ImageRenderer renderer = super.createRenderer();
96
      final RenderingHints hints = renderer.getRenderingHints();
97
      hints.putAll( RENDERING_HINTS );
98
99
      renderer.setRenderingHints( hints );
100
101
      return renderer;
102
    }
103
  }
104
105
  public static BufferedImage rasterize( final String url, final int width )
106
      throws IOException, TranscoderException {
107
    return rasterize( new URL( url ), width );
108
  }
109
110
  public static BufferedImage rasterize( final URL url, final int width )
111
      throws IOException, TranscoderException {
112
    return rasterize(
113
        (SVGDocument) mFactory.createDocument( url.toString() ), width );
114
  }
115
116
  public static BufferedImage rasterize(
117
      final SVGDocument svg, final int width ) throws TranscoderException {
118
    final var transcoder = new BufferedImageTranscoder();
119
    final var input = new TranscoderInput( svg );
120
121
    transcoder.addTranscodingHint( KEY_BACKGROUND_COLOR, WHITE );
122
    transcoder.addTranscodingHint( KEY_WIDTH, (float) width );
123
    transcoder.transcode( input, null );
124
125
    return transcoder.getBufferedImage();
126
  }
127
}
1128
A src/main/java/com/scrivenvar/preview/SVGReplacedElementFactory.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.preview;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.events.Notifier;
32
import org.apache.commons.io.FilenameUtils;
33
import org.w3c.dom.Element;
34
import org.xhtmlrenderer.extend.ReplacedElement;
35
import org.xhtmlrenderer.extend.ReplacedElementFactory;
36
import org.xhtmlrenderer.extend.UserAgentCallback;
37
import org.xhtmlrenderer.layout.LayoutContext;
38
import org.xhtmlrenderer.render.BlockBox;
39
import org.xhtmlrenderer.simple.extend.FormSubmissionListener;
40
import org.xhtmlrenderer.swing.ImageReplacedElement;
41
42
import java.awt.*;
43
44
public class SVGReplacedElementFactory
45
    implements ReplacedElementFactory {
46
47
  private final static Notifier sNotifier = Services.load( Notifier.class );
48
49
  /**
50
   * SVG filename extension.
51
   */
52
  private static final String SVG_FILE = "svg";
53
  private static final String HTML_IMAGE = "img";
54
  private static final String HTML_IMAGE_SRC = "src";
55
56
  public ReplacedElement createReplacedElement(
57
      final LayoutContext c, final BlockBox box, final UserAgentCallback uac,
58
      final int cssWidth, final int cssHeight ) {
59
    final Element e = box.getElement();
60
61
    if( e == null ) {
62
      return null;
63
    }
64
65
    final String nodeName = e.getNodeName();
66
    ReplacedElement result = null;
67
68
    if( HTML_IMAGE.equals( nodeName ) ) {
69
      final String src = e.getAttribute( HTML_IMAGE_SRC );
70
      final String ext = FilenameUtils.getExtension( src );
71
72
      if( SVG_FILE.equalsIgnoreCase( ext ) ) {
73
        try {
74
          final int width = box.getContentWidth();
75
          final Image image = SVGRasterizer.rasterize( src, width );
76
77
          final int w = image.getWidth( null );
78
          final int h = image.getHeight( null );
79
80
          result = new ImageReplacedElement( image, w, h );
81
        } catch( final Exception ex ) {
82
          getNotifier().notify( ex );
83
        }
84
      }
85
    }
86
87
    return result;
88
  }
89
90
  @Override
91
  public void reset() {
92
  }
93
94
  @Override
95
  public void remove( Element e ) {
96
  }
97
98
  @Override
99
  public void setFormSubmissionListener( FormSubmissionListener listener ) {
100
  }
101
102
  private Notifier getNotifier() {
103
    return sNotifier;
104
  }
105
}
1106
A src/main/java/com/scrivenvar/processors/AbstractProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * Responsible for transforming a document through a variety of chained
32
 * handlers. If there are conditions where this handler should not process the
33
 * entire chain, create a second handler, or split the chain into reusable
34
 * sub-chains.
35
 *
36
 * @author White Magic Software, Ltd.
37
 * @param <T> The type of object to process.
38
 */
39
public abstract class AbstractProcessor<T> implements Processor<T> {
40
41
  /**
42
   * Used while processing the entire chain; null to signify no more links.
43
   */
44
  private final Processor<T> mNext;
45
46
  /**
47
   * Constructs a succession without a successor (i.e., next is null).
48
   */
49
  protected AbstractProcessor() {
50
    this( null );
51
  }
52
53
  /**
54
   * Constructs a new default handler with a given successor.
55
   *
56
   * @param successor Use null to indicate last link in the chain.
57
   */
58
  public AbstractProcessor( final Processor<T> successor ) {
59
    mNext = successor;
60
  }
61
62
  /**
63
   * Processes links in the chain while there are successors and valid data to
64
   * process.
65
   *
66
   * @param t The object to process.
67
   */
68
  @Override
69
  public synchronized void processChain( T t ) {
70
    Processor<T> handler = this;
71
72
    while( handler != null && t != null ) {
73
      t = handler.processLink( t );
74
      handler = handler.next();
75
    }
76
  }
77
78
  @Override
79
  public Processor<T> next() {
80
    return mNext;
81
  }
82
}
183
A src/main/java/com/scrivenvar/processors/DefinitionProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import java.util.Map;
31
32
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
33
34
/**
35
 * Processes interpolated string definitions in the document and inserts
36
 * their values into the post-processed text. The default variable syntax is
37
 * {@code $variable$}.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public class DefinitionProcessor extends AbstractProcessor<String> {
42
43
  private final Map<String, String> mDefinitions;
44
45
  public DefinitionProcessor(
46
      final Processor<String> successor, final Map<String, String> map ) {
47
    super( successor );
48
    mDefinitions = map;
49
  }
50
51
  /**
52
   * Processes the given text document by replacing variables with their values.
53
   *
54
   * @param text The document text that includes variables that should be
55
   *             replaced with values when rendered as HTML.
56
   * @return The text with all variables replaced.
57
   */
58
  @Override
59
  public String processLink( final String text ) {
60
    return replace( text, getDefinitions() );
61
  }
62
63
  /**
64
   * Returns the map to use for variable substitution.
65
   *
66
   * @return A map of variable names to values.
67
   */
68
  protected Map<String, String> getDefinitions() {
69
    return mDefinitions;
70
  }
71
}
172
A src/main/java/com/scrivenvar/processors/HTMLPreviewProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.preview.HTMLPreviewPane;
31
32
/**
33
 * Responsible for notifying the HTMLPreviewPane when the succession chain has
34
 * updated. This decouples knowledge of changes to the editor panel from the
35
 * HTML preview panel as well as any processing that takes place before the
36
 * final HTML preview is rendered. This should be the last link in the processor
37
 * chain.
38
 *
39
 * @author White Magic Software, Ltd.
40
 */
41
public class HTMLPreviewProcessor extends AbstractProcessor<String> {
42
43
  // There is only one preview panel.
44
  private static HTMLPreviewPane sHtmlPreviewPane;
45
46
  /**
47
   * Constructs the end of a processing chain.
48
   *
49
   * @param htmlPreviewPane The pane to update with the post-processed document.
50
   */
51
  public HTMLPreviewProcessor( final HTMLPreviewPane htmlPreviewPane ) {
52
    sHtmlPreviewPane = htmlPreviewPane;
53
  }
54
55
  /**
56
   * Update the preview panel using HTML from the succession chain.
57
   *
58
   * @param html The document content to render in the preview pane. The HTML
59
   * should not contain a doctype, head, or body tag, only content to render
60
   * within the body.
61
   *
62
   * @return null
63
   */
64
  @Override
65
  public String processLink( final String html ) {
66
    getHtmlPreviewPane().update( html );
67
68
    // No more processing required.
69
    return null;
70
  }
71
72
  private HTMLPreviewPane getHtmlPreviewPane() {
73
    return sHtmlPreviewPane;
74
  }
75
}
176
A src/main/java/com/scrivenvar/processors/IdentityProcessor.java
1
/*
2
 * Copyright 2017 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * This is the default processor used when an unknown filename extension is
32
 * encountered.
33
 *
34
 * @author White Magic Software, Ltd.
35
 */
36
public class IdentityProcessor extends AbstractProcessor<String> {
37
38
  /**
39
   * Passes the link to the super constructor.
40
   *
41
   * @param link The next processor in the chain to use for text processing.
42
   */
43
  public IdentityProcessor( final Processor<String> link ) {
44
    super( link );
45
  }
46
47
  /**
48
   * Returns the given string, modified with "pre" tags.
49
   *
50
   * @param t The string to return, enclosed in "pre" tags.
51
   * @return The value of t wrapped in "pre" tags.
52
   */
53
  @Override
54
  public String processLink( final String t ) {
55
    return "<pre>" + t + "</pre>";
56
  }
57
}
158
A src/main/java/com/scrivenvar/processors/InlineRProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.preferences.UserPreferences;
32
import com.scrivenvar.service.Options;
33
import com.scrivenvar.service.events.Notifier;
34
35
import javax.script.ScriptEngine;
36
import javax.script.ScriptEngineManager;
37
import javax.script.ScriptException;
38
import java.nio.file.Path;
39
import java.util.LinkedHashMap;
40
import java.util.Map;
41
42
import static com.scrivenvar.Constants.*;
43
import static com.scrivenvar.Messages.get;
44
import static com.scrivenvar.decorators.RVariableDecorator.PREFIX;
45
import static com.scrivenvar.decorators.RVariableDecorator.SUFFIX;
46
import static com.scrivenvar.processors.text.TextReplacementFactory.replace;
47
import static java.lang.Math.min;
48
49
/**
50
 * Transforms a document containing R statements into Markdown.
51
 *
52
 * @author White Magic Software, Ltd.
53
 */
54
public final class InlineRProcessor extends DefinitionProcessor {
55
56
  private static final Notifier NOTIFIER = Services.load( Notifier.class );
57
  private static final Options OPTIONS = Services.load( Options.class );
58
59
  /**
60
   * Constrain memory when typing new R expressions into the document.
61
   */
62
  private static final int MAX_CACHED_R_STATEMENTS = 512;
63
64
  /**
65
   * Only one editor is open at a time.
66
   */
67
  private static final ScriptEngine ENGINE =
68
      (new ScriptEngineManager()).getEngineByName( "Renjin" );
69
70
  /**
71
   * Where to put document inline evaluated R expressions.
72
   */
73
  private final Map<String, Object> mEvalCache = new LinkedHashMap<>() {
74
    @Override
75
    protected boolean removeEldestEntry(
76
        final Map.Entry<String, Object> eldest ) {
77
      return size() > MAX_CACHED_R_STATEMENTS;
78
    }
79
  };
80
81
  /**
82
   * Constructs a processor capable of evaluating R statements.
83
   *
84
   * @param processor Subsequent link in the processing chain.
85
   * @param map       Resolved definitions map.
86
   */
87
  public InlineRProcessor(
88
      final Processor<String> processor,
89
      final Map<String, String> map ) {
90
    super( processor, map );
91
    init();
92
  }
93
94
  /**
95
   * Initialises the R code so that R can find imported libraries.
96
   */
97
  private void init() {
98
    try {
99
      final Path wd = getWorkingDirectory();
100
      final String dir = wd.toString().replace( '\\', '/' );
101
      final Map<String, String> map = getDefinitions();
102
      map.put( "$application.r.working.directory$", dir );
103
104
      final String bootstrap = getBootstrapScript();
105
106
      if( !bootstrap.isBlank() ) {
107
        eval( replace( bootstrap, map ) );
108
      }
109
    } catch( final Exception e ) {
110
      getNotifier().notify( e );
111
    }
112
  }
113
114
  /**
115
   * Evaluates all R statements in the source document and inserts the
116
   * calculated value into the generated document.
117
   *
118
   * @param text The document text that includes variables that should be
119
   *             replaced with values when rendered as HTML.
120
   * @return The generated document with output from all R statements
121
   * substituted with value returned from their execution.
122
   */
123
  @Override
124
  public String processLink( final String text ) {
125
    final int length = text.length();
126
    final int prefixLength = PREFIX.length();
127
128
    // The * 2 is a wild guess at the ratio of R statements to the length
129
    // of text produced by those statements.
130
    final StringBuilder sb = new StringBuilder( length * 2 );
131
132
    int prevIndex = 0;
133
    int currIndex = text.indexOf( PREFIX );
134
135
    while( currIndex >= 0 ) {
136
      // Copy everything up to, but not including, an R statement (`r#).
137
      sb.append( text, prevIndex, currIndex );
138
139
      // Jump to the start of the R statement.
140
      prevIndex = currIndex + prefixLength;
141
142
      // Find the statement ending (`), without indexing past the text boundary.
143
      currIndex = text.indexOf( SUFFIX, min( currIndex + 1, length ) );
144
145
      // Only evaluate inline R statements that have end delimiters.
146
      if( currIndex > 1 ) {
147
        // Extract the inline R statement to be evaluated.
148
        final String r = text.substring( prevIndex, currIndex );
149
150
        // Pass the R statement into the R engine for evaluation.
151
        try {
152
          final Object result = evalText( r );
153
154
          // Append the string representation of the result into the text.
155
          sb.append( result );
156
        } catch( final Exception e ) {
157
          // If the string couldn't be parsed using R, append the statement
158
          // that failed to parse, instead of its evaluated value.
159
          sb.append( PREFIX ).append( r ).append( SUFFIX );
160
161
          // Tell the user that there was a problem.
162
          getNotifier().notify(
163
              get( STATUS_PARSE_ERROR, e.getMessage(), currIndex )
164
          );
165
        }
166
167
        // Retain the R statement's ending position in the text.
168
        prevIndex = currIndex + 1;
169
      }
170
171
      // Find the start of the next inline R statement.
172
      currIndex = text.indexOf( PREFIX, min( currIndex + 1, length ) );
173
    }
174
175
    // Copy from the previous index to the end of the string.
176
    return sb.append( text.substring( min( prevIndex, length ) ) ).toString();
177
  }
178
179
  /**
180
   * Look up an R expression from the cache then return the resulting object.
181
   * If the R expression hasn't been cached, it'll first be evalulated.
182
   *
183
   * @param r The expression to evaluate.
184
   * @return The object resulting from the evaluation.
185
   */
186
  private Object evalText( final String r ) {
187
    return mEvalCache.computeIfAbsent( r, v -> eval( r ) );
188
  }
189
190
  /**
191
   * Evaluate an R expression and return the resulting object.
192
   *
193
   * @param r The expression to evaluate.
194
   * @return The object resulting from the evaluation.
195
   */
196
  private Object eval( final String r ) {
197
    try {
198
      return getScriptEngine().eval( r );
199
    } catch( final ScriptException e ) {
200
      getNotifier().notify( e );
201
      return "";
202
    }
203
  }
204
205
  /**
206
   * This will return the given path if not null, otherwise it will return
207
   * the path to the user's directory.
208
   *
209
   * @return A non-null path.
210
   */
211
  private Path getWorkingDirectory() {
212
    return getUserPreferences().getRDirectory().toPath();
213
  }
214
215
  /**
216
   * Loads the R init script from the application's persisted preferences.
217
   *
218
   * @return A non-null String, possibly empty.
219
   */
220
  private String getBootstrapScript() {
221
    return getUserPreferences().getRScript();
222
  }
223
224
  private UserPreferences getUserPreferences() {
225
    return getOptions().getUserPreferences();
226
  }
227
228
  private ScriptEngine getScriptEngine() {
229
    return ENGINE;
230
  }
231
232
  private Notifier getNotifier() {
233
    return NOTIFIER;
234
  }
235
236
  private Options getOptions() {
237
    return OPTIONS;
238
  }
239
}
1240
A src/main/java/com/scrivenvar/processors/Processor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
/**
31
 * Responsible for processing documents from one known format to another.
32
 *
33
 * @param <T> The type of processor to create.
34
 * @author White Magic Software, Ltd.
35
 */
36
public interface Processor<T> {
37
38
  /**
39
   * Provided so that the chain can be invoked from any link using a given
40
   * value. This should be called automatically by a superclass so that
41
   * the links in the chain need only implement the processLink method.
42
   *
43
   * @param t The value to pass along to each link in the chain.
44
   */
45
  void processChain( T t );
46
47
  /**
48
   * Processes the given content providing a transformation from one document
49
   * format into another. For example, this could convert from XML to text using
50
   * an XSLT processor, or from markdown to HTML.
51
   *
52
   * @param t The type of object to process.
53
   * @return The post-processed document, or null if processing should stop.
54
   */
55
  T processLink( T t );
56
57
  /**
58
   * Adds a document processor to call after this processor finishes processing
59
   * the document given to the process method.
60
   *
61
   * @return The processor that should transform the document after this
62
   * instance has finished processing.
63
   */
64
  Processor<T> next();
65
}
166
A src/main/java/com/scrivenvar/processors/ProcessorFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.AbstractFileFactory;
31
import com.scrivenvar.FileEditorTab;
32
import com.scrivenvar.preview.HTMLPreviewPane;
33
import com.scrivenvar.processors.markdown.MarkdownProcessor;
34
35
import java.nio.file.Path;
36
import java.util.Map;
37
38
/**
39
 * Responsible for creating processors capable of parsing, transforming,
40
 * interpolating, and rendering known file types.
41
 *
42
 * @author White Magic Software, Ltd.
43
 */
44
public class ProcessorFactory extends AbstractFileFactory {
45
46
  private final HTMLPreviewPane mPreviewPane;
47
  private final Map<String, String> mResolvedMap;
48
  private final Processor<String> mMarkdownProcessor;
49
50
  /**
51
   * Constructs a factory with the ability to create processors that can perform
52
   * text and caret processing to generate a final preview.
53
   *
54
   * @param previewPane Where the final output is rendered.
55
   * @param resolvedMap Flat map of definitions to replace before final render.
56
   */
57
  public ProcessorFactory(
58
      final HTMLPreviewPane previewPane,
59
      final Map<String, String> resolvedMap ) {
60
    mPreviewPane = previewPane;
61
    mResolvedMap = resolvedMap;
62
    mMarkdownProcessor = createMarkdownProcessor();
63
  }
64
65
  /**
66
   * Creates a processor suitable for parsing and rendering the file opened at
67
   * the given tab.
68
   *
69
   * @param tab The tab containing a text editor, path, and caret position.
70
   * @return A processor that can render the given tab's text.
71
   */
72
  public Processor<String> createProcessor( final FileEditorTab tab ) {
73
    final Path path = tab.getPath();
74
    final Processor<String> processor;
75
76
    switch( lookup( path ) ) {
77
      case RMARKDOWN:
78
        processor = createRProcessor();
79
        break;
80
81
      case SOURCE:
82
        processor = createMarkdownDefinitionProcessor();
83
        break;
84
85
      case XML:
86
        processor = createXMLProcessor( tab );
87
        break;
88
89
      case RXML:
90
        processor = createRXMLProcessor( tab );
91
        break;
92
93
      default:
94
        processor = createIdentityProcessor();
95
        break;
96
    }
97
98
    return processor;
99
  }
100
101
  private Processor<String> createHTMLPreviewProcessor() {
102
    return new HTMLPreviewProcessor( getPreviewPane() );
103
  }
104
105
  /**
106
   * Creates and links the processors at the end of the processing chain.
107
   *
108
   * @return A markdown, caret replacement, and preview pane processor chain.
109
   */
110
  private Processor<String> createMarkdownProcessor() {
111
    final var hpp = createHTMLPreviewProcessor();
112
    return new MarkdownProcessor( hpp, getPreviewPane().getPath() );
113
  }
114
115
  protected Processor<String> createIdentityProcessor() {
116
    final var hpp = createHTMLPreviewProcessor();
117
    return new IdentityProcessor( hpp );
118
  }
119
120
  protected Processor<String> createDefinitionProcessor(
121
      final Processor<String> p ) {
122
    return new DefinitionProcessor( p, getResolvedMap() );
123
  }
124
125
  protected Processor<String> createMarkdownDefinitionProcessor() {
126
    final var tpc = getCommonProcessor();
127
    return createDefinitionProcessor( tpc );
128
  }
129
130
  protected Processor<String> createXMLProcessor( final FileEditorTab tab ) {
131
    final var tpc = getCommonProcessor();
132
    final var xmlp = new XMLProcessor( tpc, tab.getPath() );
133
    return createDefinitionProcessor( xmlp );
134
  }
135
136
  protected Processor<String> createRProcessor() {
137
    final var tpc = getCommonProcessor();
138
    final var rp = new InlineRProcessor( tpc, getResolvedMap() );
139
    return new RVariableProcessor( rp, getResolvedMap() );
140
  }
141
142
  protected Processor<String> createRXMLProcessor( final FileEditorTab tab ) {
143
    final var tpc = getCommonProcessor();
144
    final var xmlp = new XMLProcessor( tpc, tab.getPath() );
145
    final var rp = new InlineRProcessor( xmlp, getResolvedMap() );
146
    return new RVariableProcessor( rp, getResolvedMap() );
147
  }
148
149
  private HTMLPreviewPane getPreviewPane() {
150
    return mPreviewPane;
151
  }
152
153
  /**
154
   * Returns the variable map of interpolated definitions.
155
   *
156
   * @return A map to help dereference variables.
157
   */
158
  private Map<String, String> getResolvedMap() {
159
    return mResolvedMap;
160
  }
161
162
  /**
163
   * Returns a processor common to all processors: markdown, caret position
164
   * token replacer, and an HTML preview renderer.
165
   *
166
   * @return Processors at the end of the processing chain.
167
   */
168
  private Processor<String> getCommonProcessor() {
169
    return mMarkdownProcessor;
170
  }
171
}
1172
A src/main/java/com/scrivenvar/processors/RVariableProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import java.util.HashMap;
31
import java.util.Map;
32
33
/**
34
 * Converts the keys of the resolved map from default form to R form, then
35
 * performs a substitution on the text. The default R variable syntax is
36
 * {@code v$tree$leaf}.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class RVariableProcessor extends DefinitionProcessor {
41
42
  public RVariableProcessor(
43
      final Processor<String> rp, final Map<String, String> map ) {
44
    super( rp, map );
45
  }
46
47
  /**
48
   * Returns the R-based version of the interpolated variable definitions.
49
   *
50
   * @return Variable names transmogrified from the default syntax to R syntax.
51
   */
52
  @Override
53
  protected Map<String, String> getDefinitions() {
54
    return toR( super.getDefinitions() );
55
  }
56
57
  /**
58
   * Converts the given map from regular variables to R variables.
59
   *
60
   * @param map Map of variable names to values.
61
   * @return Map of R variables.
62
   */
63
  private Map<String, String> toR( final Map<String, String> map ) {
64
    final Map<String, String> rMap = new HashMap<>( map.size() );
65
66
    for( final String key : map.keySet() ) {
67
      rMap.put( toRKey( key ), toRValue( map.get( key ) ) );
68
    }
69
70
    return rMap;
71
  }
72
73
  /**
74
   * Transforms a variable name from $tree.branch.leaf$ to v$tree$branch$leaf
75
   * form.
76
   *
77
   * @param key The variable name to transform, can be empty but not null.
78
   * @return The transformed variable name.
79
   */
80
  private String toRKey( final String key ) {
81
    // Replace all the periods with dollar symbols.
82
    final StringBuilder sb = new StringBuilder( 'v' + key );
83
    final int length = sb.length();
84
85
    // Replace all periods with dollar symbols. Normally we'd check i >= 0,
86
    // but the prepended 'v' is always going to be a 'v', not a dot.
87
    for( int i = length - 1; i > 0; i-- ) {
88
      if( sb.charAt( i ) == '.' ) {
89
        sb.setCharAt( i, '$' );
90
      }
91
    }
92
93
    // The length is always at least 1 (the 'v'), so bounds aren't broken here.
94
    sb.setLength( length - 1 );
95
96
    return sb.toString();
97
  }
98
99
  private String toRValue( final String value ) {
100
    return '\'' + escape( value, '\'', "\\'" ) + '\'';
101
  }
102
103
  /**
104
   * TODO: Make generic method for replacing text.
105
   *
106
   * @param haystack Search this string for the needle, must not be null.
107
   * @param needle   The character to find in the haystack.
108
   * @param thread   Replace the needle with this text, if the needle is found.
109
   * @return The haystack with the all instances of needle replaced with thread.
110
   */
111
  @SuppressWarnings("SameParameterValue")
112
  private String escape(
113
      final String haystack, final char needle, final String thread ) {
114
    int end = haystack.indexOf( needle );
115
116
    if( end < 0 ) {
117
      return haystack;
118
    }
119
120
    final int length = haystack.length();
121
    int start = 0;
122
123
    // Replace up to 32 occurrences before the string reallocates its buffer.
124
    final StringBuilder sb = new StringBuilder( length + 32 );
125
126
    while( end >= 0 ) {
127
      sb.append( haystack, start, end ).append( thread );
128
      start = end + 1;
129
      end = haystack.indexOf( needle, start );
130
    }
131
132
    return sb.append( haystack.substring( start ) ).toString();
133
  }
134
}
1135
A src/main/java/com/scrivenvar/processors/XMLProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.Snitch;
32
import net.sf.saxon.TransformerFactoryImpl;
33
import net.sf.saxon.trans.XPathException;
34
35
import javax.xml.stream.XMLEventReader;
36
import javax.xml.stream.XMLInputFactory;
37
import javax.xml.stream.XMLStreamException;
38
import javax.xml.stream.events.ProcessingInstruction;
39
import javax.xml.stream.events.XMLEvent;
40
import javax.xml.transform.*;
41
import javax.xml.transform.stream.StreamResult;
42
import javax.xml.transform.stream.StreamSource;
43
import java.io.File;
44
import java.io.Reader;
45
import java.io.StringReader;
46
import java.io.StringWriter;
47
import java.nio.file.Path;
48
import java.nio.file.Paths;
49
50
import static net.sf.saxon.tree.util.ProcInstParser.getPseudoAttribute;
51
52
/**
53
 * Transforms an XML document. The XML document must have a stylesheet specified
54
 * as part of its processing instructions, such as:
55
 *
56
 * <code>xml-stylesheet type="text/xsl" href="markdown.xsl"</code>
57
 * <p>
58
 * The XSL must transform the XML document into Markdown, or another format
59
 * recognized by the next link on the chain.
60
 *
61
 * @author White Magic Software, Ltd.
62
 */
63
public class XMLProcessor extends AbstractProcessor<String>
64
    implements ErrorListener {
65
66
  private final Snitch snitch = Services.load( Snitch.class );
67
68
  private XMLInputFactory xmlInputFactory;
69
  private TransformerFactory transformerFactory;
70
  private Transformer transformer;
71
72
  private Path path;
73
74
  /**
75
   * Constructs an XML processor that can transform an XML document into another
76
   * format based on the XSL file specified as a processing instruction. The
77
   * path must point to the directory where the XSL file is found, which implies
78
   * that they must be in the same directory.
79
   *
80
   * @param processor Next link in the processing chain.
81
   * @param path      The path to the XML file content to be processed.
82
   */
83
  public XMLProcessor( final Processor<String> processor, final Path path ) {
84
    super( processor );
85
    setPath( path );
86
  }
87
88
  /**
89
   * Transforms the given XML text into another form (typically Markdown).
90
   *
91
   * @param text The text to transform, can be empty, cannot be null.
92
   * @return The transformed text, or empty if text is empty.
93
   */
94
  @Override
95
  public String processLink( final String text ) {
96
    try {
97
      return text.isEmpty() ? text : transform( text );
98
    } catch( final Exception ex ) {
99
      throw new RuntimeException( ex );
100
    }
101
  }
102
103
  /**
104
   * Performs an XSL transformation on the given XML text. The XML text must
105
   * have a processing instruction that points to the XSL template file to use
106
   * for the transformation.
107
   *
108
   * @param text The text to transform.
109
   * @return The transformed text.
110
   */
111
  private String transform( final String text ) throws Exception {
112
    // Extract the XML stylesheet processing instruction.
113
    final String template = getXsltFilename( text );
114
    final Path xsl = getXslPath( template );
115
116
    try(
117
        final StringWriter output = new StringWriter( text.length() );
118
        final StringReader input = new StringReader( text ) ) {
119
120
      // Listen for external file modification events.
121
      getSnitch().listen( xsl );
122
123
      getTransformer( xsl ).transform(
124
          new StreamSource( input ),
125
          new StreamResult( output )
126
      );
127
128
      return output.toString();
129
    }
130
  }
131
132
  /**
133
   * Returns an XSL transformer ready to transform an XML document using the
134
   * XSLT file specified by the given path. If the path is already known then
135
   * this will return the associated transformer.
136
   *
137
   * @param xsl The path to an XSLT file.
138
   * @return A transformer that will transform XML documents using the given
139
   * XSLT file.
140
   * @throws TransformerConfigurationException Could not instantiate the
141
   *                                           transformer.
142
   */
143
  private Transformer getTransformer( final Path xsl )
144
      throws TransformerConfigurationException {
145
    if( this.transformer == null ) {
146
      this.transformer = createTransformer( xsl );
147
    }
148
149
    return this.transformer;
150
  }
151
152
  /**
153
   * Creates a configured transformer ready to run.
154
   *
155
   * @param xsl The stylesheet to use for transforming XML documents.
156
   * @return The edited XML document transformed into another format (usually
157
   * markdown).
158
   * @throws TransformerConfigurationException Could not create the transformer.
159
   */
160
  protected Transformer createTransformer( final Path xsl )
161
      throws TransformerConfigurationException {
162
    final Source xslt = new StreamSource( xsl.toFile() );
163
164
    return getTransformerFactory().newTransformer( xslt );
165
  }
166
167
  private Path getXslPath( final String filename ) {
168
    final Path xmlPath = getPath();
169
    final File xmlDirectory = xmlPath.toFile().getParentFile();
170
171
    return Paths.get( xmlDirectory.getPath(), filename );
172
  }
173
174
  /**
175
   * Given XML text, this will use a StAX pull reader to obtain the XML
176
   * stylesheet processing instruction. This will throw a parse exception if the
177
   * href pseudo-attribute filename value cannot be found.
178
   *
179
   * @param xml The XML containing an xml-stylesheet processing instruction.
180
   * @return The href pseudo-attribute value.
181
   * @throws XMLStreamException Could not parse the XML file.
182
   */
183
  private String getXsltFilename( final String xml )
184
      throws XMLStreamException, XPathException {
185
186
    String result = "";
187
188
    try( final StringReader sr = new StringReader( xml ) ) {
189
      boolean found = false;
190
      int count = 0;
191
      final XMLEventReader reader = createXMLEventReader( sr );
192
193
      // If the processing instruction wasn't found in the first 10 lines,
194
      // fail fast. This should iterate twice through the loop.
195
      while( !found && reader.hasNext() && count++ < 10 ) {
196
        final XMLEvent event = reader.nextEvent();
197
198
        if( event.isProcessingInstruction() ) {
199
          final ProcessingInstruction pi = (ProcessingInstruction) event;
200
          final String target = pi.getTarget();
201
202
          if( "xml-stylesheet".equalsIgnoreCase( target ) ) {
203
            result = getPseudoAttribute( pi.getData(), "href" );
204
            found = true;
205
          }
206
        }
207
      }
208
    }
209
210
    return result;
211
  }
212
213
  private XMLEventReader createXMLEventReader( final Reader reader )
214
      throws XMLStreamException {
215
    return getXMLInputFactory().createXMLEventReader( reader );
216
  }
217
218
  private synchronized XMLInputFactory getXMLInputFactory() {
219
    if( this.xmlInputFactory == null ) {
220
      this.xmlInputFactory = createXMLInputFactory();
221
    }
222
223
    return this.xmlInputFactory;
224
  }
225
226
  private XMLInputFactory createXMLInputFactory() {
227
    return XMLInputFactory.newInstance();
228
  }
229
230
  private synchronized TransformerFactory getTransformerFactory() {
231
    if( this.transformerFactory == null ) {
232
      this.transformerFactory = createTransformerFactory();
233
    }
234
235
    return this.transformerFactory;
236
  }
237
238
  /**
239
   * Returns a high-performance XSLT 2 transformation engine.
240
   *
241
   * @return An XSL transforming engine.
242
   */
243
  private TransformerFactory createTransformerFactory() {
244
    final TransformerFactory factory = new TransformerFactoryImpl();
245
246
    // Bubble problems up to the user interface, rather than standard error.
247
    factory.setErrorListener( this );
248
249
    return factory;
250
  }
251
252
  /**
253
   * Called when the XSL transformer issues a warning.
254
   *
255
   * @param ex The problem the transformer encountered.
256
   */
257
  @Override
258
  public void warning( final TransformerException ex ) {
259
    throw new RuntimeException( ex );
260
  }
261
262
  /**
263
   * Called when the XSL transformer issues an error.
264
   *
265
   * @param ex The problem the transformer encountered.
266
   */
267
  @Override
268
  public void error( final TransformerException ex ) {
269
    throw new RuntimeException( ex );
270
  }
271
272
  /**
273
   * Called when the XSL transformer issues a fatal error, which is probably
274
   * a bit over-dramatic a method name.
275
   *
276
   * @param ex The problem the transformer encountered.
277
   */
278
  @Override
279
  public void fatalError( final TransformerException ex ) {
280
    throw new RuntimeException( ex );
281
  }
282
283
  private void setPath( final Path path ) {
284
    this.path = path;
285
  }
286
287
  private Path getPath() {
288
    return this.path;
289
  }
290
291
  private Snitch getSnitch() {
292
    return this.snitch;
293
  }
294
}
1295
A src/main/java/com/scrivenvar/processors/markdown/ImageLinkExtension.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.markdown;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.preferences.UserPreferences;
32
import com.scrivenvar.service.Options;
33
import com.scrivenvar.service.events.Notifier;
34
import com.scrivenvar.util.ProtocolResolver;
35
import com.vladsch.flexmark.ast.Image;
36
import com.vladsch.flexmark.html.HtmlRenderer;
37
import com.vladsch.flexmark.html.IndependentLinkResolverFactory;
38
import com.vladsch.flexmark.html.LinkResolver;
39
import com.vladsch.flexmark.html.renderer.LinkResolverBasicContext;
40
import com.vladsch.flexmark.html.renderer.LinkStatus;
41
import com.vladsch.flexmark.html.renderer.ResolvedLink;
42
import com.vladsch.flexmark.util.ast.Node;
43
import com.vladsch.flexmark.util.data.MutableDataHolder;
44
import org.jetbrains.annotations.NotNull;
45
import org.renjin.repackaged.guava.base.Splitter;
46
47
import java.io.File;
48
import java.nio.file.Path;
49
50
import static java.lang.String.format;
51
52
/**
53
 * Responsible for ensuring that images can be rendered relative to a path.
54
 * This allows images to be located virtually anywhere.
55
 *
56
 * @author White Magic Software, Ltd.
57
 */
58
public class ImageLinkExtension implements HtmlRenderer.HtmlRendererExtension {
59
  private final static Options sOptions = Services.load( Options.class );
60
  private final static Notifier sNotifier = Services.load( Notifier.class );
61
62
  /**
63
   * Creates an extension capable of using a relative path to embed images.
64
   *
65
   * @param path The {@link Path} to the file being edited; the parent path
66
   *             is the starting location of the relative image directory.
67
   * @return The new {@link ImageLinkExtension}, never {@code null}.
68
   */
69
  public static ImageLinkExtension create( final Path path ) {
70
    return new ImageLinkExtension( path );
71
  }
72
73
  private class Factory extends IndependentLinkResolverFactory {
74
    @Override
75
    public @NotNull LinkResolver apply(
76
        @NotNull final LinkResolverBasicContext context ) {
77
      return new ImageLinkResolver();
78
    }
79
  }
80
81
  private class ImageLinkResolver implements LinkResolver {
82
    private final UserPreferences mUserPref = getUserPreferences();
83
    private final String mImagePrefix =
84
        mUserPref.getImagesDirectory().toString();
85
    private final String mImageSuffixes = mUserPref.getImagesOrder();
86
87
    public ImageLinkResolver() {
88
    }
89
90
    // you can also set/clear/modify attributes through
91
    // ResolvedLink.getAttributes() and
92
    // ResolvedLink.getNonNullAttributes()
93
    @NotNull
94
    @Override
95
    public ResolvedLink resolveLink(
96
        @NotNull final Node node,
97
        @NotNull final LinkResolverBasicContext context,
98
        @NotNull final ResolvedLink link ) {
99
      return node instanceof Image ? resolve( link ) : link;
100
    }
101
102
    @NotNull
103
    private ResolvedLink resolve( @NotNull final ResolvedLink link ) {
104
      String url = link.getUrl();
105
106
      try {
107
        final String imageFile = format( "%s/%s", getImagePrefix(), url );
108
        final String suffixes = getImageSuffixes();
109
        final String editDir = getEditDirectory();
110
111
        for( final String ext : Splitter.on( ' ' ).split( suffixes ) ) {
112
          final String imagePath = format(
113
              "%s/%s.%s", editDir, imageFile, ext );
114
          final File file = new File( imagePath );
115
116
          if( file.exists() ) {
117
            url = file.toString();
118
            break;
119
          }
120
        }
121
122
        final String protocol = ProtocolResolver.getProtocol( url );
123
        if( "file".equals( protocol ) ) {
124
          url = "file://" + url;
125
        }
126
127
        return link.withStatus( LinkStatus.VALID ).withUrl( url );
128
      } catch( final Exception e ) {
129
        getNotifier().notify( e );
130
      }
131
132
      return link;
133
    }
134
135
    private String getImagePrefix() {
136
      return mImagePrefix;
137
    }
138
139
    private String getImageSuffixes() {
140
      return mImageSuffixes;
141
    }
142
143
    private String getEditDirectory() {
144
      return mPath.getParent().toString();
145
    }
146
  }
147
148
  private final Path mPath;
149
150
  private ImageLinkExtension( final Path path ) {
151
    mPath = path;
152
  }
153
154
  @Override
155
  public void rendererOptions( @NotNull final MutableDataHolder options ) {
156
  }
157
158
  @Override
159
  public void extend(
160
      final HtmlRenderer.Builder rendererBuilder,
161
      @NotNull final String rendererType ) {
162
    rendererBuilder.linkResolverFactory( new Factory() );
163
  }
164
165
  private UserPreferences getUserPreferences() {
166
    return getOptions().getUserPreferences();
167
  }
168
169
  private Options getOptions() {
170
    return sOptions;
171
  }
172
173
  private Notifier getNotifier() {
174
    return sNotifier;
175
  }
176
}
1177
A src/main/java/com/scrivenvar/processors/markdown/MarkdownProcessor.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.markdown;
29
30
import com.scrivenvar.processors.AbstractProcessor;
31
import com.scrivenvar.processors.Processor;
32
import com.vladsch.flexmark.ext.gfm.strikethrough.StrikethroughSubscriptExtension;
33
import com.vladsch.flexmark.ext.superscript.SuperscriptExtension;
34
import com.vladsch.flexmark.ext.tables.TablesExtension;
35
import com.vladsch.flexmark.html.HtmlRenderer;
36
import com.vladsch.flexmark.parser.Parser;
37
import com.vladsch.flexmark.util.ast.IParse;
38
import com.vladsch.flexmark.util.ast.Node;
39
import com.vladsch.flexmark.util.misc.Extension;
40
41
import java.nio.file.Path;
42
import java.util.ArrayList;
43
import java.util.Collection;
44
45
import static com.scrivenvar.Constants.USER_DIRECTORY;
46
47
/**
48
 * Responsible for parsing a Markdown document and rendering it as HTML.
49
 *
50
 * @author White Magic Software, Ltd.
51
 */
52
public class MarkdownProcessor extends AbstractProcessor<String> {
53
54
  private final HtmlRenderer mRenderer;
55
  private final IParse mParser;
56
57
  public MarkdownProcessor(
58
      final Processor<String> successor ) {
59
    this( successor, Path.of( USER_DIRECTORY ) );
60
  }
61
62
  /**
63
   * Constructs a new Markdown processor that can create HTML documents.
64
   *
65
   * @param successor Usually the HTML Preview Processor.
66
   */
67
  public MarkdownProcessor(
68
      final Processor<String> successor, final Path path ) {
69
    super( successor );
70
71
    final Collection<Extension> extensions = new ArrayList<>();
72
    extensions.add( TablesExtension.create() );
73
    extensions.add( SuperscriptExtension.create() );
74
    extensions.add( StrikethroughSubscriptExtension.create() );
75
    extensions.add( ImageLinkExtension.create( path ) );
76
77
    mRenderer = HtmlRenderer.builder().extensions( extensions ).build();
78
    mParser = Parser.builder().extensions( extensions ).build();
79
  }
80
81
  /**
82
   * Converts the given Markdown string into HTML, without the doctype, html,
83
   * head, and body tags.
84
   *
85
   * @param markdown The string to convert from Markdown to HTML.
86
   * @return The HTML representation of the Markdown document.
87
   */
88
  @Override
89
  public String processLink( final String markdown ) {
90
    return toHtml( markdown );
91
  }
92
93
  /**
94
   * Returns the AST in the form of a node for the given markdown document. This
95
   * can be used, for example, to determine if a hyperlink exists inside of a
96
   * paragraph.
97
   *
98
   * @param markdown The markdown to convert into an AST.
99
   * @return The markdown AST for the given text (usually a paragraph).
100
   */
101
  public Node toNode( final String markdown ) {
102
    return parse( markdown );
103
  }
104
105
  /**
106
   * Helper method to create an AST given some markdown.
107
   *
108
   * @param markdown The markdown to parse.
109
   * @return The root node of the markdown tree.
110
   */
111
  private Node parse( final String markdown ) {
112
    return getParser().parse( markdown );
113
  }
114
115
  /**
116
   * Converts a string of markdown into HTML.
117
   *
118
   * @param markdown The markdown text to convert to HTML, must not be null.
119
   * @return The markdown rendered as an HTML document.
120
   */
121
  private String toHtml( final String markdown ) {
122
    return getRenderer().render( parse( markdown ) );
123
  }
124
125
  /**
126
   * Creates the Markdown document processor.
127
   *
128
   * @return A Parser that can build an abstract syntax tree.
129
   */
130
  private IParse getParser() {
131
    return mParser;
132
  }
133
134
  private HtmlRenderer getRenderer() {
135
    return mRenderer;
136
  }
137
}
1138
A src/main/java/com/scrivenvar/processors/text/AbstractTextReplacer.java
1
/*
2
 * The MIT License
3
 *
4
 * Copyright 2016 .
5
 *
6
 * Permission is hereby granted, free of charge, to any person obtaining a copy
7
 * of this software and associated documentation files (the "Software"), to deal
8
 * in the Software without restriction, including without limitation the rights
9
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
 * copies of the Software, and to permit persons to whom the Software is
11
 * furnished to do so, subject to the following conditions:
12
 *
13
 * The above copyright notice and this permission notice shall be included in
14
 * all copies or substantial portions of the Software.
15
 *
16
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
22
 * THE SOFTWARE.
23
 */
24
package com.scrivenvar.processors.text;
25
26
import java.util.Map;
27
28
/**
29
 * Responsible for common behaviour across all text replacer implementations.
30
 *
31
 * @author White Magic Software, Ltd.
32
 */
33
public abstract class AbstractTextReplacer implements TextReplacer {
34
35
  /**
36
   * Default (empty) constructor.
37
   */
38
  protected AbstractTextReplacer() {
39
  }
40
41
  protected String[] keys( final Map<String, String> map ) {
42
    return map.keySet().toArray( new String[ 0 ] );
43
  }
44
45
  protected String[] values( final Map<String, String> map ) {
46
    return map.values().toArray( new String[ 0 ] );
47
  }
48
}
149
A src/main/java/com/scrivenvar/processors/text/AhoCorasickReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
import org.ahocorasick.trie.Emit;
32
import org.ahocorasick.trie.Trie.TrieBuilder;
33
import static org.ahocorasick.trie.Trie.builder;
34
35
/**
36
 * Replaces text using an Aho-Corasick algorithm.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public class AhoCorasickReplacer extends AbstractTextReplacer {
41
42
  /**
43
   * Default (empty) constructor.
44
   */
45
  protected AhoCorasickReplacer() {
46
  }
47
48
  @Override
49
  public String replace( final String text, final Map<String, String> map ) {
50
    // Create a buffer sufficiently large that re-allocations are minimized.
51
    final StringBuilder sb = new StringBuilder( (int)(text.length() * 1.25) );
52
53
    // The TrieBuilder should only match whole words and ignore overlaps (there
54
    // shouldn't be any).
55
    final TrieBuilder builder = builder().onlyWholeWords().ignoreOverlaps();
56
57
    for( final String key : keys( map ) ) {
58
      builder.addKeyword( key );
59
    }
60
61
    int index = 0;
62
63
    // Replace all instances with dereferenced variables.
64
    for( final Emit emit : builder.build().parseText( text ) ) {
65
      sb.append( text, index, emit.getStart() );
66
      sb.append( map.get( emit.getKeyword() ) );
67
      index = emit.getEnd() + 1;
68
    }
69
70
    // Add the remainder of the string (contains no more matches).
71
    sb.append( text.substring( index ) );
72
73
    return sb.toString();
74
  }
75
}
176
A src/main/java/com/scrivenvar/processors/text/StringUtilsReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
import static org.apache.commons.lang3.StringUtils.replaceEach;
33
34
/**
35
 * Replaces text using Apache's StringUtils.replaceEach method.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public class StringUtilsReplacer extends AbstractTextReplacer {
40
41
  /**
42
   * Default (empty) constructor.
43
   */
44
  protected StringUtilsReplacer() {
45
  }
46
47
  @Override
48
  public String replace( final String text, final Map<String, String> map ) {
49
    return replaceEach( text, keys( map ), values( map ) );
50
  }
51
}
152
A src/main/java/com/scrivenvar/processors/text/TextReplacementFactory.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Used to generate a class capable of efficiently replacing variable
34
 * definitions with their values.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public final class TextReplacementFactory {
39
40
  private final static TextReplacer APACHE = new StringUtilsReplacer();
41
  private final static TextReplacer AHO_CORASICK = new AhoCorasickReplacer();
42
43
  /**
44
   * Returns a text search/replacement instance that is reasonably optimal for
45
   * the given length of text.
46
   *
47
   * @param length The length of text that requires some search and replacing.
48
   *
49
   * @return A class that can search and replace text with utmost expediency.
50
   */
51
  public static TextReplacer getTextReplacer( final int length ) {
52
    // After about 1,500 characters, the StringUtils implementation is less
53
    // performant than the Aho-Corsick implementation.
54
    //
55
    // See http://stackoverflow.com/a/40836618/59087
56
    return length < 1500 ? APACHE : AHO_CORASICK;
57
  }
58
59
  /**
60
   * Convenience method to instantiate a suitable text replacer algorithm and
61
   * perform a replacement using the given map. At this point, the values should
62
   * be already dereferenced and ready to be substituted verbatim; any
63
   * recursively defined values must have been interpolated previously.
64
   *
65
   * @param text The text containing zero or more variables to replace.
66
   * @param map The map of variables to their dereferenced values.
67
   *
68
   * @return The text with all variables replaced.
69
   */
70
  public static String replace(
71
    final String text, final Map<String, String> map ) {
72
    return getTextReplacer( text.length() ).replace( text, map );
73
  }
74
}
175
A src/main/java/com/scrivenvar/processors/text/TextReplacer.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.processors.text;
29
30
import java.util.Map;
31
32
/**
33
 * Defines the ability to replace text given a set of keys and values.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public interface TextReplacer {
38
39
  /**
40
   * Searches through the given text for any of the keys given in the map and
41
   * replaces the keys that appear in the text with the key's corresponding
42
   * value.
43
   *
44
   * @param text The text that contains zero or more keys.
45
   * @param map  The set of keys mapped to replacement values.
46
   * @return The given text with all keys replaced with corresponding values.
47
   */
48
  String replace( String text, Map<String, String> map );
49
}
150
A src/main/java/com/scrivenvar/service/Options.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import com.scrivenvar.preferences.UserPreferences;
31
32
import java.util.prefs.BackingStoreException;
33
import java.util.prefs.Preferences;
34
35
/**
36
 * Responsible for persisting options.
37
 *
38
 * @author White Magic Software, Ltd.
39
 */
40
public interface Options extends Service {
41
42
  /**
43
   * Returns a reference to the persistent settings that may be configured
44
   * through the UI.
45
   *
46
   * @return A valid {@link UserPreferences} instance, never {@code null}.
47
   */
48
  UserPreferences getUserPreferences();
49
50
  /**
51
   * Returns the {@link Preferences} that persist settings that cannot
52
   * be configured via the user interface.
53
   *
54
   * @return A valid {@link Preferences} instance, never {@code null}.
55
   */
56
  Preferences getState();
57
58
  /**
59
   * Stores the key and value into the user preferences to be loaded the next
60
   * time the application is launched.
61
   *
62
   * @param key   Name of the key to persist along with its value.
63
   * @param value Value to associate with the key.
64
   * @throws BackingStoreException Could not persist the change.
65
   */
66
  void put( String key, String value ) throws BackingStoreException;
67
68
  /**
69
   * Retrieves the value for a key in the user preferences.
70
   *
71
   * @param key          Retrieve the value of this key.
72
   * @param defaultValue The value to return in the event that the given key has
73
   *                     no associated value.
74
   * @return The value associated with the key.
75
   */
76
  String get( String key, String defaultValue );
77
78
  /**
79
   * Retrieves the value for a key in the user preferences. This will return
80
   * the empty string if the value cannot be found.
81
   *
82
   * @param key The key to find in the preferences.
83
   * @return A non-null, possibly empty value for the key.
84
   */
85
  String get( String key );
86
}
187
A src/main/java/com/scrivenvar/service/Service.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
/**
31
 * All services inherit from this one.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public interface Service {
36
}
137
A src/main/java/com/scrivenvar/service/Settings.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.util.Iterator;
31
import java.util.List;
32
33
/**
34
 * Defines how settings and options can be retrieved.
35
 *
36
 * @author White Magic Software, Ltd.
37
 */
38
public interface Settings extends Service {
39
40
  /**
41
   * Returns a setting property or its default value.
42
   *
43
   * @param property     The property key name to obtain its value.
44
   * @param defaultValue The default value to return iff the property cannot be
45
   *                     found.
46
   * @return The property value for the given property key.
47
   */
48
  String getSetting( String property, String defaultValue );
49
50
  /**
51
   * Returns a setting property or its default value.
52
   *
53
   * @param property     The property key name to obtain its value.
54
   * @param defaultValue The default value to return iff the property cannot be
55
   *                     found.
56
   * @return The property value for the given property key.
57
   */
58
  int getSetting( String property, int defaultValue );
59
60
  /**
61
   * Returns a list of property names that begin with the given prefix. The
62
   * prefix is included in any matching results. This will return keys that
63
   * either match the prefix or start with the prefix followed by a dot ('.').
64
   * For example, a prefix value of <code>the.property.name</code> will likely
65
   * return the expected results, but <code>the.property.name.</code> (note the
66
   * extraneous period) will probably not.
67
   *
68
   * @param prefix The prefix to compare against each property name.
69
   * @return The list of property names that have the given prefix.
70
   */
71
  Iterator<String> getKeys( final String prefix );
72
73
  /**
74
   * Convert the generic list of property objects into strings.
75
   *
76
   * @param property The property value to coerce.
77
   * @param defaults The defaults values to use should the property be unset.
78
   * @return The list of properties coerced from objects to strings.
79
   */
80
  List<String> getStringSettingList( String property, List<String> defaults );
81
82
  /**
83
   * Converts the generic list of property objects into strings.
84
   *
85
   * @param property The property value to coerce.
86
   * @return The list of properties coerced from objects to strings.
87
   */
88
  List<String> getStringSettingList( String property );
89
90
  /**
91
   * Changes key's value. This will clear the old value before setting the
92
   * new value so that the old value is erased, not changed into a list.
93
   *
94
   * @param key   The property key name to obtain its value.
95
   * @param value The new value to set.
96
   */
97
  void putSetting( String key, String value );
98
}
199
A src/main/java/com/scrivenvar/service/Snitch.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service;
29
30
import java.io.IOException;
31
import java.nio.file.Path;
32
import java.util.Observer;
33
34
/**
35
 * Listens for changes to file system files and directories.
36
 *
37
 * @author White Magic Software, Ltd.
38
 */
39
public interface Snitch extends Service, Runnable {
40
41
  /**
42
   * Adds an observer to the set of observers for this object, provided that it
43
   * is not the same as some observer already in the set. The order in which
44
   * notifications will be delivered to multiple observers is not specified.
45
   *
46
   * @param o The object to receive changed events for when monitored files
47
   *          are changed.
48
   */
49
  void addObserver( Observer o );
50
51
  /**
52
   * Listens for changes to the path. If the path specifies a file, then only
53
   * notifications pertaining to that file are sent. Otherwise, change events
54
   * for the directory that contains the file are sent. This method must allow
55
   * for multiple calls to the same file without incurring additional listeners
56
   * or events.
57
   *
58
   * @param file Send notifications when this file changes, can be null.
59
   * @throws IOException Couldn't create a watcher for the given file.
60
   */
61
  void listen( Path file ) throws IOException;
62
63
  /**
64
   * Removes the given file from the notifications list.
65
   *
66
   * @param file The file to stop monitoring for any changes, can be null.
67
   */
68
  void ignore( Path file );
69
70
  /**
71
   * Stop listening for events.
72
   */
73
  void stop();
74
}
175
A src/main/java/com/scrivenvar/service/events/Notification.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
/**
31
 * Represents a message that contains a title and content.
32
 *
33
 * @author White Magic Software, Ltd.
34
 */
35
public interface Notification {
36
37
  /**
38
   * Alert title.
39
   *
40
   * @return A non-null string to use as alert message title.
41
   */
42
  String getTitle();
43
44
  /**
45
   * Alert message content.
46
   *
47
   * @return A non-null string that contains information for the user.
48
   */
49
  String getContent();
50
}
151
A src/main/java/com/scrivenvar/service/events/Notifier.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events;
29
30
import javafx.scene.control.Alert;
31
import javafx.scene.control.ButtonType;
32
import javafx.stage.Window;
33
34
import java.io.File;
35
import java.io.FileWriter;
36
import java.io.IOException;
37
import java.io.PrintWriter;
38
import java.util.Observer;
39
40
/**
41
 * Provides the application with a uniform way to notify the user of events.
42
 *
43
 * @author White Magic Software, Ltd.
44
 */
45
public interface Notifier {
46
47
  ButtonType YES = ButtonType.YES;
48
  ButtonType NO = ButtonType.NO;
49
  ButtonType CANCEL = ButtonType.CANCEL;
50
51
  /**
52
   * Notifies the user of a problem.
53
   *
54
   * @param message The problem description.
55
   */
56
  void notify( final String message );
57
58
  /**
59
   * Notifies the user about the exception.
60
   *
61
   * @param ex The exception containing a message to show to the user.
62
   */
63
  default void notify( final Exception ex ) {
64
    assert ex != null;
65
66
    log( ex );
67
    notify( ex.getMessage() );
68
  }
69
70
  /**
71
   * Writes the exception to a log file. The log file should be written
72
   * in the System's temporary directory.
73
   *
74
   * @param ex The exception to show in the status bar and log to a file.
75
   */
76
  default void log( final Exception ex ) {
77
    try(
78
        final FileWriter fw = new FileWriter( getLogPath(), true );
79
        final PrintWriter pw = new PrintWriter( fw )
80
    ) {
81
      ex.printStackTrace( pw );
82
    } catch( final IOException ioe ) {
83
      // The notify method will display the message on the status
84
      // bar.
85
    }
86
  }
87
88
  /**
89
   * Returns the fully qualified path to the log file to write to when
90
   * an exception occurs.
91
   *
92
   * @return Location of the log file for writing unexpected exceptions.
93
   */
94
  File getLogPath();
95
96
  /**
97
   * Causes any displayed notifications to disappear.
98
   */
99
  void clear();
100
101
  /**
102
   * Constructs a default alert message text for a modal alert dialog.
103
   *
104
   * @param title   The dialog box message title.
105
   * @param message The dialog box message content (needs formatting).
106
   * @param args    The arguments to the message content that must be formatted.
107
   * @return The message suitable for building a modal alert dialog.
108
   */
109
  Notification createNotification(
110
      String title,
111
      String message,
112
      Object... args );
113
114
  /**
115
   * Creates an alert of alert type error with a message showing the cause of
116
   * the error.
117
   *
118
   * @param parent  Dialog box owner (for modal purposes).
119
   * @param message The error message, title, and possibly more details.
120
   * @return A modal alert dialog box ready to display using showAndWait.
121
   */
122
  Alert createError( Window parent, Notification message );
123
124
  /**
125
   * Creates an alert of alert type confirmation with Yes/No/Cancel buttons.
126
   *
127
   * @param parent  Dialog box owner (for modal purposes).
128
   * @param message The message, title, and possibly more details.
129
   * @return A modal alert dialog box ready to display using showAndWait.
130
   */
131
  Alert createConfirmation( Window parent, Notification message );
132
133
  /**
134
   * Adds an observer to the list of objects that receive notifications about
135
   * error messages to be presented to the user.
136
   *
137
   * @param observer The observer instance to notify.
138
   */
139
  void addObserver( Observer observer );
140
}
1141
A src/main/java/com/scrivenvar/service/events/impl/ButtonOrderPane.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.Services;
31
import com.scrivenvar.service.Settings;
32
import javafx.scene.Node;
33
import javafx.scene.control.ButtonBar;
34
import static javafx.scene.control.ButtonBar.BUTTON_ORDER_WINDOWS;
35
import javafx.scene.control.DialogPane;
36
37
/**
38
 * Ensures a consistent button order for alert dialogs across platforms (because
39
 * the default button order on Linux defies all logic).
40
 *
41
 * @author White Magic Software, Ltd.
42
 */
43
public class ButtonOrderPane extends DialogPane {
44
45
  @Override
46
  protected Node createButtonBar() {
47
    final ButtonBar node = (ButtonBar)super.createButtonBar();
48
    node.setButtonOrder( getButtonOrder() );
49
    return node;
50
  }
51
52
  private String getButtonOrder() {
53
    return getSetting( "dialog.alert.button.order.windows", BUTTON_ORDER_WINDOWS );
54
  }
55
56
  @SuppressWarnings("SameParameterValue")
57
  private String getSetting( final String key, final String defaultValue ) {
58
    return getSettings().getSetting( key, defaultValue );
59
  }
60
61
  private Settings getSettings() {
62
    return Services.load( Settings.class );
63
  }
64
}
165
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotification.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.Notification;
31
32
import java.text.MessageFormat;
33
34
/**
35
 * @author White Magic Software, Ltd.
36
 */
37
public class DefaultNotification implements Notification {
38
39
  private final String title;
40
  private final String content;
41
42
  /**
43
   * Constructs default message text for a notification.
44
   *
45
   * @param title   The message title.
46
   * @param message The message content (needs formatting).
47
   * @param args    The arguments to the message content that must be formatted.
48
   */
49
  public DefaultNotification(
50
      final String title,
51
      final String message,
52
      final Object... args ) {
53
    this.title = title;
54
    this.content = MessageFormat.format( message, args );
55
  }
56
57
  @Override
58
  public String getTitle() {
59
    return this.title;
60
  }
61
62
  @Override
63
  public String getContent() {
64
    return this.content;
65
  }
66
}
167
A src/main/java/com/scrivenvar/service/events/impl/DefaultNotifier.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.events.impl;
29
30
import com.scrivenvar.service.events.Notification;
31
import com.scrivenvar.service.events.Notifier;
32
import javafx.scene.control.Alert;
33
import javafx.scene.control.Alert.AlertType;
34
import javafx.stage.Window;
35
36
import java.io.File;
37
import java.nio.file.Paths;
38
import java.util.Observable;
39
40
import static com.scrivenvar.Constants.APP_TITLE;
41
import static com.scrivenvar.Constants.STATUS_BAR_OK;
42
import static com.scrivenvar.Messages.get;
43
import static javafx.scene.control.Alert.AlertType.CONFIRMATION;
44
import static javafx.scene.control.Alert.AlertType.ERROR;
45
46
/**
47
 * Provides the ability to notify the user of problems.
48
 *
49
 * @author White Magic Software, Ltd.
50
 */
51
public final class DefaultNotifier extends Observable implements Notifier {
52
53
  private final static String OK = get( STATUS_BAR_OK, "OK" );
54
55
  /**
56
   * Notifies all observer instances of the given message.
57
   *
58
   * @param message The text to display to the user.
59
   */
60
  @Override
61
  public void notify( final String message ) {
62
    if( message != null && !message.isBlank() ) {
63
      setChanged();
64
      notifyObservers( message );
65
    }
66
  }
67
68
  @Override
69
  public void clear() {
70
    notify( OK );
71
  }
72
73
  /**
74
   * Contains all the information that the user needs to know about a problem.
75
   *
76
   * @param title   The context for the message.
77
   * @param message The message content (formatted with the given args).
78
   * @param args    Parameters for the message content.
79
   * @return A notification instance, never null.
80
   */
81
  @Override
82
  public Notification createNotification(
83
      final String title,
84
      final String message,
85
      final Object... args ) {
86
    return new DefaultNotification( title, message, args );
87
  }
88
89
  private Alert createAlertDialog(
90
      final Window parent,
91
      final AlertType alertType,
92
      final Notification message ) {
93
94
    final Alert alert = new Alert( alertType );
95
96
    alert.setDialogPane( new ButtonOrderPane() );
97
    alert.setTitle( message.getTitle() );
98
    alert.setHeaderText( null );
99
    alert.setContentText( message.getContent() );
100
    alert.initOwner( parent );
101
102
    return alert;
103
  }
104
105
  @Override
106
  public Alert createConfirmation( final Window parent,
107
                                   final Notification message ) {
108
    final Alert alert = createAlertDialog( parent, CONFIRMATION, message );
109
110
    alert.getButtonTypes().setAll( YES, NO, CANCEL );
111
112
    return alert;
113
  }
114
115
  @Override
116
  public Alert createError( final Window parent, final Notification message ) {
117
    return createAlertDialog( parent, ERROR, message );
118
  }
119
120
  @Override
121
  public File getLogPath() {
122
    return Paths.get(
123
        System.getProperty( "java.io.tmpdir" ), APP_TITLE + ".log" ).toFile();
124
  }
125
}
1126
A src/main/java/com/scrivenvar/service/impl/DefaultOptions.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.service.impl;
28
29
import com.scrivenvar.preferences.UserPreferences;
30
import com.scrivenvar.service.Options;
31
32
import java.util.prefs.BackingStoreException;
33
import java.util.prefs.Preferences;
34
35
import static com.scrivenvar.Constants.PREFS_ROOT;
36
import static com.scrivenvar.Constants.PREFS_STATE;
37
import static java.util.prefs.Preferences.userRoot;
38
39
/**
40
 * Persistent options user can change at runtime.
41
 *
42
 * @author Karl Tauber and White Magic Software, Ltd.
43
 */
44
public class DefaultOptions implements Options {
45
  private final UserPreferences mPreferences = new UserPreferences();
46
47
  public DefaultOptions() {
48
  }
49
50
  /**
51
   * This will throw IllegalArgumentException if the value exceeds the maximum
52
   * preferences value length.
53
   *
54
   * @param key   The name of the key to associate with the value.
55
   * @param value The value to persist.
56
   * @throws BackingStoreException New value not persisted.
57
   */
58
  @Override
59
  public void put( final String key, final String value )
60
      throws BackingStoreException {
61
    getState().put( key, value );
62
    getState().flush();
63
  }
64
65
  @Override
66
  public String get( final String key, final String value ) {
67
    return getState().get( key, value );
68
  }
69
70
  @Override
71
  public String get( final String key ) {
72
    return get( key, "" );
73
  }
74
75
  private Preferences getRootPreferences() {
76
    return userRoot().node( PREFS_ROOT );
77
  }
78
79
  @Override
80
  public Preferences getState() {
81
    return getRootPreferences().node( PREFS_STATE );
82
  }
83
84
  @Override
85
  public UserPreferences getUserPreferences() {
86
    return mPreferences;
87
  }
88
}
189
A src/main/java/com/scrivenvar/service/impl/DefaultSettings.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.impl;
29
30
import com.scrivenvar.service.Settings;
31
import org.apache.commons.configuration2.PropertiesConfiguration;
32
import org.apache.commons.configuration2.convert.DefaultListDelimiterHandler;
33
import org.apache.commons.configuration2.convert.ListDelimiterHandler;
34
import org.apache.commons.configuration2.ex.ConfigurationException;
35
36
import java.io.IOException;
37
import java.io.InputStreamReader;
38
import java.io.Reader;
39
import java.net.URL;
40
import java.nio.charset.Charset;
41
import java.util.Iterator;
42
import java.util.List;
43
44
import static com.scrivenvar.Constants.SETTINGS_NAME;
45
46
/**
47
 * Responsible for loading settings that help avoid hard-coded assumptions.
48
 *
49
 * @author White Magic Software, Ltd.
50
 */
51
public class DefaultSettings implements Settings {
52
53
  private static final char VALUE_SEPARATOR = ',';
54
55
  private PropertiesConfiguration properties;
56
57
  public DefaultSettings() throws ConfigurationException {
58
    setProperties( createProperties() );
59
  }
60
61
  /**
62
   * Returns the value of a string property.
63
   *
64
   * @param property     The property key.
65
   * @param defaultValue The value to return if no property key has been set.
66
   * @return The property key value, or defaultValue when no key found.
67
   */
68
  @Override
69
  public String getSetting( final String property, final String defaultValue ) {
70
    return getSettings().getString( property, defaultValue );
71
  }
72
73
  /**
74
   * Returns the value of a string property.
75
   *
76
   * @param property     The property key.
77
   * @param defaultValue The value to return if no property key has been set.
78
   * @return The property key value, or defaultValue when no key found.
79
   */
80
  @Override
81
  public int getSetting( final String property, final int defaultValue ) {
82
    return getSettings().getInt( property, defaultValue );
83
  }
84
85
  /**
86
   * Changes key's value. This will clear the old value before setting the new
87
   * value so that the old value is erased, not changed into a list.
88
   *
89
   * @param key   The property key name to obtain its value.
90
   * @param value The new value to set.
91
   */
92
  @Override
93
  public void putSetting( final String key, final String value ) {
94
    getSettings().clearProperty( key );
95
    getSettings().addProperty( key, value );
96
  }
97
98
  /**
99
   * Convert the generic list of property objects into strings.
100
   *
101
   * @param property The property value to coerce.
102
   * @param defaults The defaults values to use should the property be unset.
103
   * @return The list of properties coerced from objects to strings.
104
   */
105
  @Override
106
  public List<String> getStringSettingList(
107
      final String property, final List<String> defaults ) {
108
    return getSettings().getList( String.class, property, defaults );
109
  }
110
111
  /**
112
   * Convert a list of property objects into strings, with no default value.
113
   *
114
   * @param property The property value to coerce.
115
   * @return The list of properties coerced from objects to strings.
116
   */
117
  @Override
118
  public List<String> getStringSettingList( final String property ) {
119
    return getStringSettingList( property, null );
120
  }
121
122
  /**
123
   * Returns a list of property names that begin with the given prefix.
124
   *
125
   * @param prefix The prefix to compare against each property name.
126
   * @return The list of property names that have the given prefix.
127
   */
128
  @Override
129
  public Iterator<String> getKeys( final String prefix ) {
130
    return getSettings().getKeys( prefix );
131
  }
132
133
  private PropertiesConfiguration createProperties()
134
      throws ConfigurationException {
135
136
    final URL url = getPropertySource();
137
    final PropertiesConfiguration configuration = new PropertiesConfiguration();
138
139
    if( url != null ) {
140
      try( final Reader r = new InputStreamReader( url.openStream(),
141
                                                   getDefaultEncoding() ) ) {
142
        configuration.setListDelimiterHandler( createListDelimiterHandler() );
143
        configuration.read( r );
144
145
      } catch( final IOException ex ) {
146
        throw new RuntimeException( new ConfigurationException( ex ) );
147
      }
148
    }
149
150
    return configuration;
151
  }
152
153
  protected Charset getDefaultEncoding() {
154
    return Charset.defaultCharset();
155
  }
156
157
  protected ListDelimiterHandler createListDelimiterHandler() {
158
    return new DefaultListDelimiterHandler( VALUE_SEPARATOR );
159
  }
160
161
  private URL getPropertySource() {
162
    return DefaultSettings.class.getResource( getSettingsFilename() );
163
  }
164
165
  private String getSettingsFilename() {
166
    return SETTINGS_NAME;
167
  }
168
169
  private void setProperties( final PropertiesConfiguration configuration ) {
170
    this.properties = configuration;
171
  }
172
173
  private PropertiesConfiguration getSettings() {
174
    return this.properties;
175
  }
176
}
1177
A src/main/java/com/scrivenvar/service/impl/DefaultSnitch.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.service.impl;
29
30
import com.scrivenvar.service.Snitch;
31
32
import java.io.IOException;
33
import java.nio.file.*;
34
import java.util.Collections;
35
import java.util.Map;
36
import java.util.Observable;
37
import java.util.Set;
38
import java.util.concurrent.ConcurrentHashMap;
39
40
import static com.scrivenvar.Constants.APP_WATCHDOG_TIMEOUT;
41
import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
42
43
/**
44
 * Listens for file changes. Other classes can register paths to be monitored
45
 * and listen for changes to those paths.
46
 *
47
 * @author White Magic Software, Ltd.
48
 */
49
public class DefaultSnitch extends Observable implements Snitch {
50
51
  /**
52
   * Service for listening to directories for modifications.
53
   */
54
  private WatchService watchService;
55
56
  /**
57
   * Directories being monitored for changes.
58
   */
59
  private Map<WatchKey, Path> keys;
60
61
  /**
62
   * Files that will kick off notification events if modified.
63
   */
64
  private Set<Path> eavesdropped;
65
66
  /**
67
   * Set to true when running; set to false to stop listening.
68
   */
69
  private volatile boolean listening;
70
71
  public DefaultSnitch() {
72
  }
73
74
  @Override
75
  public void stop() {
76
    setListening( false );
77
  }
78
79
  /**
80
   * Adds a listener to the list of files to watch for changes. If the file is
81
   * already in the monitored list, this will return immediately.
82
   *
83
   * @param file Path to a file to watch for changes.
84
   * @throws IOException The file could not be monitored.
85
   */
86
  @Override
87
  public void listen( final Path file ) throws IOException {
88
    if( file != null && getEavesdropped().add( file ) ) {
89
      final Path dir = toDirectory( file );
90
      final WatchKey key = dir.register( getWatchService(), ENTRY_MODIFY );
91
92
      getWatchMap().put( key, dir );
93
    }
94
  }
95
96
  /**
97
   * Returns the given path to a file (or directory) as a directory. If the
98
   * given path is already a directory, it is returned. Otherwise, this returns
99
   * the directory that contains the file. This will fail if the file is stored
100
   * in the root folder.
101
   *
102
   * @param path The file to return as a directory, which should always be the
103
   *             case.
104
   * @return The given path as a directory, if a file, otherwise the path
105
   * itself.
106
   */
107
  private Path toDirectory( final Path path ) {
108
    return Files.isDirectory( path )
109
        ? path
110
        : path.toFile().getParentFile().toPath();
111
  }
112
113
  /**
114
   * Stop listening to the given file for change events. This fails silently.
115
   *
116
   * @param file The file to no longer monitor for changes.
117
   */
118
  @Override
119
  public void ignore( final Path file ) {
120
    if( file != null ) {
121
      final Path directory = toDirectory( file );
122
123
      // Remove all occurrences (there should be only one).
124
      getWatchMap().values().removeAll( Collections.singleton( directory ) );
125
126
      // Remove all occurrences (there can be only one).
127
      getEavesdropped().remove( file );
128
    }
129
  }
130
131
  /**
132
   * Loops until stop is called, or the application is terminated.
133
   */
134
  @Override
135
  @SuppressWarnings("BusyWait")
136
  public void run() {
137
    setListening( true );
138
139
    while( isListening() ) {
140
      try {
141
        final WatchKey key = getWatchService().take();
142
        final Path path = get( key );
143
144
        // Prevent receiving two separate ENTRY_MODIFY events: file modified
145
        // and timestamp updated. Instead, receive one ENTRY_MODIFY event
146
        // with two counts.
147
        Thread.sleep( APP_WATCHDOG_TIMEOUT );
148
149
        for( final WatchEvent<?> event : key.pollEvents() ) {
150
          final Path changed = path.resolve( (Path) event.context() );
151
152
          if( event.kind() == ENTRY_MODIFY && isListening( changed ) ) {
153
            setChanged();
154
            notifyObservers( changed );
155
          }
156
        }
157
158
        if( !key.reset() ) {
159
          ignore( path );
160
        }
161
      } catch( final IOException | InterruptedException ex ) {
162
        // Stop eavesdropping.
163
        setListening( false );
164
      }
165
    }
166
  }
167
168
  /**
169
   * Returns true if the list of files being listened to for changes contains
170
   * the given file.
171
   *
172
   * @param file Path to a system file.
173
   * @return true The given file is being monitored for changes.
174
   */
175
  private boolean isListening( final Path file ) {
176
    return getEavesdropped().contains( file );
177
  }
178
179
  /**
180
   * Returns a path for a given watch key.
181
   *
182
   * @param key The key to lookup its corresponding path.
183
   * @return The path for the given key.
184
   */
185
  private Path get( final WatchKey key ) {
186
    return getWatchMap().get( key );
187
  }
188
189
  private synchronized Map<WatchKey, Path> getWatchMap() {
190
    if( this.keys == null ) {
191
      this.keys = createWatchKeys();
192
    }
193
194
    return this.keys;
195
  }
196
197
  protected Map<WatchKey, Path> createWatchKeys() {
198
    return new ConcurrentHashMap<>();
199
  }
200
201
  /**
202
   * Returns a list of files that, when changed, will kick off a notification.
203
   *
204
   * @return A non-null, possibly empty, list of files.
205
   */
206
  private synchronized Set<Path> getEavesdropped() {
207
    if( this.eavesdropped == null ) {
208
      this.eavesdropped = createEavesdropped();
209
    }
210
211
    return this.eavesdropped;
212
  }
213
214
  protected Set<Path> createEavesdropped() {
215
    return ConcurrentHashMap.newKeySet();
216
  }
217
218
  /**
219
   * The existing watch service, or a new instance if null.
220
   *
221
   * @return A valid WatchService instance, never null.
222
   * @throws IOException Could not create a new watch service.
223
   */
224
  private synchronized WatchService getWatchService() throws IOException {
225
    if( this.watchService == null ) {
226
      this.watchService = createWatchService();
227
    }
228
229
    return this.watchService;
230
  }
231
232
  protected WatchService createWatchService() throws IOException {
233
    final FileSystem fileSystem = FileSystems.getDefault();
234
    return fileSystem.newWatchService();
235
  }
236
237
  /**
238
   * Answers whether the loop should continue executing.
239
   *
240
   * @return true The internal listening loop should continue listening for file
241
   * modification events.
242
   */
243
  protected boolean isListening() {
244
    return this.listening;
245
  }
246
247
  /**
248
   * Requests the snitch to stop eavesdropping on file changes.
249
   *
250
   * @param listening Use true to indicate the service should stop running.
251
   */
252
  private void setListening( final boolean listening ) {
253
    this.listening = listening;
254
  }
255
}
1256
A src/main/java/com/scrivenvar/util/Action.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber and White Magic Software, Ltd.
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import de.jensd.fx.glyphs.GlyphIcons;
30
import javafx.beans.value.ObservableBooleanValue;
31
import javafx.event.ActionEvent;
32
import javafx.event.EventHandler;
33
import javafx.scene.input.KeyCombination;
34
35
/**
36
 * Simple action class
37
 *
38
 * @author Karl Tauber
39
 * @author White Magic Software, Ltd.
40
 */
41
public class Action {
42
  public final String text;
43
  public final KeyCombination accelerator;
44
  public final GlyphIcons icon;
45
  public final EventHandler<ActionEvent> action;
46
  public final ObservableBooleanValue disable;
47
48
  public Action(
49
      final String text,
50
      final String accelerator,
51
      final GlyphIcons icon,
52
      final EventHandler<ActionEvent> action,
53
      final ObservableBooleanValue disable ) {
54
55
    this.text = text;
56
    this.accelerator = accelerator == null ?
57
        null : KeyCombination.valueOf( accelerator );
58
    this.icon = icon;
59
    this.action = action;
60
    this.disable = disable;
61
  }
62
}
163
A src/main/java/com/scrivenvar/util/ActionBuilder.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
import com.scrivenvar.Messages;
31
import de.jensd.fx.glyphs.GlyphIcons;
32
import javafx.beans.value.ObservableBooleanValue;
33
import javafx.event.ActionEvent;
34
import javafx.event.EventHandler;
35
36
/**
37
 * Provides a fluent interface around constructing actions so that duplication
38
 * can be avoided.
39
 */
40
public class ActionBuilder {
41
  private String mText;
42
  private String mAccelerator;
43
  private GlyphIcons mIcon;
44
  private EventHandler<ActionEvent> mAction;
45
  private ObservableBooleanValue mDisable;
46
47
  /**
48
   * Sets the action text based on a resource bundle key.
49
   *
50
   * @param key The key to look up in the {@link Messages}.
51
   * @return The corresponding value, or the key name if none found.
52
   */
53
  public ActionBuilder setText( final String key ) {
54
    mText = Messages.get( key, key );
55
    return this;
56
  }
57
58
  public ActionBuilder setAccelerator( final String accelerator ) {
59
    mAccelerator = accelerator;
60
    return this;
61
  }
62
63
  public ActionBuilder setIcon( final GlyphIcons icon ) {
64
    mIcon = icon;
65
    return this;
66
  }
67
68
  public ActionBuilder setAction( final EventHandler<ActionEvent> action ) {
69
    mAction = action;
70
    return this;
71
  }
72
73
  public ActionBuilder setDisable( final ObservableBooleanValue disable ) {
74
    mDisable = disable;
75
    return this;
76
  }
77
78
  public Action build() {
79
    return new Action( mText, mAccelerator, mIcon, mAction, mDisable );
80
  }
81
}
182
A src/main/java/com/scrivenvar/util/ActionUtils.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import de.jensd.fx.glyphs.fontawesome.utils.FontAwesomeIconFactory;
30
import javafx.scene.Node;
31
import javafx.scene.control.Button;
32
import javafx.scene.control.Menu;
33
import javafx.scene.control.MenuItem;
34
import javafx.scene.control.Separator;
35
import javafx.scene.control.SeparatorMenuItem;
36
import javafx.scene.control.ToolBar;
37
import javafx.scene.control.Tooltip;
38
39
/**
40
 * Action utilities
41
 *
42
 * @author Karl Tauber
43
 * @author White Magic Software, Ltd.
44
 */
45
public class ActionUtils {
46
47
  public static Menu createMenu( final String text, final Action... actions ) {
48
    return new Menu( text, null, createMenuItems( actions ) );
49
  }
50
51
  public static MenuItem[] createMenuItems( final Action... actions ) {
52
    final MenuItem[] menuItems = new MenuItem[ actions.length ];
53
54
    for( int i = 0; i < actions.length; i++ ) {
55
      menuItems[ i ] = (actions[ i ] != null)
56
          ? createMenuItem( actions[ i ] )
57
          : new SeparatorMenuItem();
58
    }
59
60
    return menuItems;
61
  }
62
63
  public static MenuItem createMenuItem( final Action action ) {
64
    final MenuItem menuItem = new MenuItem( action.text );
65
66
    if( action.accelerator != null ) {
67
      menuItem.setAccelerator( action.accelerator );
68
    }
69
70
    if( action.icon != null ) {
71
      menuItem.setGraphic(
72
          FontAwesomeIconFactory.get().createIcon( action.icon ) );
73
    }
74
75
    menuItem.setOnAction( action.action );
76
77
    if( action.disable != null ) {
78
      menuItem.disableProperty().bind( action.disable );
79
    }
80
81
    menuItem.setMnemonicParsing( true );
82
83
    return menuItem;
84
  }
85
86
  public static ToolBar createToolBar( final Action... actions ) {
87
    return new ToolBar( createToolBarButtons( actions ) );
88
  }
89
90
  public static Node[] createToolBarButtons( final Action... actions ) {
91
    Node[] buttons = new Node[ actions.length ];
92
    for( int i = 0; i < actions.length; i++ ) {
93
      buttons[ i ] = (actions[ i ] != null)
94
          ? createToolBarButton( actions[ i ] )
95
          : new Separator();
96
    }
97
    return buttons;
98
  }
99
100
  public static Button createToolBarButton( final Action action ) {
101
    final Button button = new Button();
102
    button.setGraphic(
103
        FontAwesomeIconFactory
104
            .get()
105
            .createIcon( action.icon, "1.2em" ) );
106
107
    String tooltip = action.text;
108
109
    if( tooltip.endsWith( "..." ) ) {
110
      tooltip = tooltip.substring( 0, tooltip.length() - 3 );
111
    }
112
113
    if( action.accelerator != null ) {
114
      tooltip += " (" + action.accelerator.getDisplayText() + ')';
115
    }
116
117
    button.setTooltip( new Tooltip( tooltip ) );
118
    button.setFocusTraversable( false );
119
    button.setOnAction( action.action );
120
121
    if( action.disable != null ) {
122
      button.disableProperty().bind( action.disable );
123
    }
124
125
    return button;
126
  }
127
}
1128
A src/main/java/com/scrivenvar/util/Lists.java
1
/*
2
 * Copyright 2016 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.util;
29
30
import java.util.List;
31
32
/**
33
 * Convenience class that provides a clearer API for obtaining list elements.
34
 *
35
 * @author White Magic Software, Ltd.
36
 */
37
public final class Lists {
38
39
  private Lists() {
40
  }
41
42
  /**
43
   * Returns the first item in the given list, or null if not found.
44
   *
45
   * @param <T> The generic list type.
46
   * @param list The list that may have a first item.
47
   *
48
   * @return null if the list is null or there is no first item.
49
   */
50
  public static <T> T getFirst( final List<T> list ) {
51
    return getFirst( list, null );
52
  }
53
54
  /**
55
   * Returns the last item in the given list, or null if not found.
56
   *
57
   * @param <T> The generic list type.
58
   * @param list The list that may have a last item.
59
   *
60
   * @return null if the list is null or there is no last item.
61
   */
62
  public static <T> T getLast( final List<T> list ) {
63
    return getLast( list, null );
64
  }
65
66
  /**
67
   * Returns the first item in the given list, or t if not found.
68
   *
69
   * @param <T> The generic list type.
70
   * @param list The list that may have a first item.
71
   * @param t The default return value.
72
   *
73
   * @return null if the list is null or there is no first item.
74
   */
75
  public static <T> T getFirst( final List<T> list, final T t ) {
76
    return isEmpty( list ) ? t : list.get( 0 );
77
  }
78
79
  /**
80
   * Returns the last item in the given list, or t if not found.
81
   *
82
   * @param <T> The generic list type.
83
   * @param list The list that may have a last item.
84
   * @param t The default return value.
85
   *
86
   * @return null if the list is null or there is no last item.
87
   */
88
  public static <T> T getLast( final List<T> list, final T t ) {
89
    return isEmpty( list ) ? t : list.get( list.size() - 1 );
90
  }
91
92
  /**
93
   * Returns true if the given list is null or empty.
94
   *
95
   * @param <T> The generic list type.
96
   * @param list The list that has a last item.
97
   *
98
   * @return true The list is empty.
99
   */
100
  public static <T> boolean isEmpty( final List<T> list ) {
101
    return list == null || list.isEmpty();
102
  }
103
}
1104
A src/main/java/com/scrivenvar/util/ProtocolResolver.java
1
package com.scrivenvar.util;
2
3
import java.io.File;
4
import java.net.URI;
5
import java.net.URL;
6
7
import static com.scrivenvar.Constants.DEFINITION_PROTOCOL_UNKNOWN;
8
9
/**
10
 * Responsible for determining the protocol of a resource.
11
 */
12
public class ProtocolResolver {
13
  /**
14
   * Returns the protocol for a given URI or filename.
15
   *
16
   * @param resource Determine the protocol for this URI or filename.
17
   * @return The protocol for the given source.
18
   */
19
  public static String getProtocol( final String resource ) {
20
    String protocol;
21
22
    try {
23
      final URI uri = new URI( resource );
24
25
      if( uri.isAbsolute() ) {
26
        protocol = uri.getScheme();
27
      }
28
      else {
29
        final URL url = new URL( resource );
30
        protocol = url.getProtocol();
31
      }
32
    } catch( final Exception e ) {
33
      // Could be HTTP, HTTPS?
34
      if( resource.startsWith( "//" ) ) {
35
        throw new IllegalArgumentException( "Relative context: " + resource );
36
      }
37
      else {
38
        final File file = new File( resource );
39
        protocol = getProtocol( file );
40
      }
41
    }
42
43
    return protocol;
44
  }
45
46
  /**
47
   * Returns the protocol for a given file.
48
   *
49
   * @param file Determine the protocol for this file.
50
   * @return The protocol for the given file.
51
   */
52
  public static String getProtocol( final File file ) {
53
    String result;
54
55
    try {
56
      result = file.toURI().toURL().getProtocol();
57
    } catch( final Exception e ) {
58
      result = DEFINITION_PROTOCOL_UNKNOWN;
59
    }
60
61
    return result;
62
  }
63
}
164
A src/main/java/com/scrivenvar/util/StageState.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import java.util.prefs.Preferences;
30
31
import javafx.application.Platform;
32
import javafx.scene.shape.Rectangle;
33
import javafx.stage.Stage;
34
import javafx.stage.WindowEvent;
35
36
/**
37
 * Saves and restores Stage state (window bounds, maximized, fullScreen).
38
 *
39
 * @author Karl Tauber
40
 */
41
public class StageState {
42
43
  public static final String K_PANE_SPLIT_DEFINITION = "pane.split.definition";
44
  public static final String K_PANE_SPLIT_EDITOR = "pane.split.editor";
45
  public static final String K_PANE_SPLIT_PREVIEW = "pane.split.preview";
46
47
  private final Stage mStage;
48
  private final Preferences mState;
49
50
  private Rectangle normalBounds;
51
  private boolean runLaterPending;
52
53
  public StageState( final Stage stage, final Preferences state ) {
54
    mStage = stage;
55
    mState = state;
56
57
    restore();
58
59
    stage.addEventHandler( WindowEvent.WINDOW_HIDING, e -> save() );
60
61
    stage.xProperty().addListener( ( ob, o, n ) -> boundsChanged() );
62
    stage.yProperty().addListener( ( ob, o, n ) -> boundsChanged() );
63
    stage.widthProperty().addListener( ( ob, o, n ) -> boundsChanged() );
64
    stage.heightProperty().addListener( ( ob, o, n ) -> boundsChanged() );
65
  }
66
67
  private void save() {
68
    final Rectangle bounds = isNormalState() ? getStageBounds() : normalBounds;
69
70
    if( bounds != null ) {
71
      mState.putDouble( "windowX", bounds.getX() );
72
      mState.putDouble( "windowY", bounds.getY() );
73
      mState.putDouble( "windowWidth", bounds.getWidth() );
74
      mState.putDouble( "windowHeight", bounds.getHeight() );
75
    }
76
77
    mState.putBoolean( "windowMaximized", mStage.isMaximized() );
78
    mState.putBoolean( "windowFullScreen", mStage.isFullScreen() );
79
  }
80
81
  private void restore() {
82
    final double x = mState.getDouble( "windowX", Double.NaN );
83
    final double y = mState.getDouble( "windowY", Double.NaN );
84
    final double w = mState.getDouble( "windowWidth", Double.NaN );
85
    final double h = mState.getDouble( "windowHeight", Double.NaN );
86
    final boolean maximized = mState.getBoolean( "windowMaximized", false );
87
    final boolean fullScreen = mState.getBoolean( "windowFullScreen", false );
88
89
    if( !Double.isNaN( x ) && !Double.isNaN( y ) ) {
90
      mStage.setX( x );
91
      mStage.setY( y );
92
    } // else: default behavior is center on screen
93
94
    if( !Double.isNaN( w ) && !Double.isNaN( h ) ) {
95
      mStage.setWidth( w );
96
      mStage.setHeight( h );
97
    } // else: default behavior is use scene size
98
99
    if( fullScreen != mStage.isFullScreen() ) {
100
      mStage.setFullScreen( fullScreen );
101
    }
102
103
    if( maximized != mStage.isMaximized() ) {
104
      mStage.setMaximized( maximized );
105
    }
106
  }
107
108
  /**
109
   * Remembers the window bounds when the window is not iconified, maximized or
110
   * in fullScreen.
111
   */
112
  private void boundsChanged() {
113
    // avoid too many (and useless) runLater() invocations
114
    if( runLaterPending ) {
115
      return;
116
    }
117
118
    runLaterPending = true;
119
120
    // must use runLater() to ensure that change of all properties
121
    // (x, y, width, height, iconified, maximized and fullScreen)
122
    // has finished
123
    Platform.runLater( () -> {
124
      runLaterPending = false;
125
126
      if( isNormalState() ) {
127
        normalBounds = getStageBounds();
128
      }
129
    } );
130
  }
131
132
  private boolean isNormalState() {
133
    return !mStage.isIconified() &&
134
        !mStage.isMaximized() &&
135
        !mStage.isFullScreen();
136
  }
137
138
  private Rectangle getStageBounds() {
139
    return new Rectangle(
140
        mStage.getX(),
141
        mStage.getY(),
142
        mStage.getWidth(),
143
        mStage.getHeight()
144
    );
145
  }
146
}
1147
A src/main/java/com/scrivenvar/util/Utils.java
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
package com.scrivenvar.util;
28
29
import java.util.ArrayList;
30
import java.util.prefs.Preferences;
31
32
/**
33
 * @author Karl Tauber and White Magic Software, Ltd.
34
 */
35
public class Utils {
36
37
  public static String ltrim( final String s ) {
38
    int i = 0;
39
40
    while( i < s.length() && Character.isWhitespace( s.charAt( i ) ) ) {
41
      i++;
42
    }
43
44
    return s.substring( i );
45
  }
46
47
  public static String rtrim( final String s ) {
48
    int i = s.length() - 1;
49
50
    while( i >= 0 && Character.isWhitespace( s.charAt( i ) ) ) {
51
      i--;
52
    }
53
54
    return s.substring( 0, i + 1 );
55
  }
56
57
  public static String[] getPrefsStrings( final Preferences prefs,
58
                                          String key ) {
59
    final ArrayList<String> arr = new ArrayList<>( 256 );
60
61
    for( int i = 0; i < 10000; i++ ) {
62
      final String s = prefs.get( key + (i + 1), null );
63
64
      if( s == null ) {
65
        break;
66
      }
67
68
      arr.add( s );
69
    }
70
71
    return arr.toArray( new String[ 0 ] );
72
  }
73
74
  public static void putPrefsStrings( Preferences prefs, String key,
75
                                      String[] strings ) {
76
    for( int i = 0; i < strings.length; i++ ) {
77
      prefs.put( key + (i + 1), strings[ i ] );
78
    }
79
80
    for( int i = strings.length; prefs.get( key + (i + 1),
81
                                            null ) != null; i++ ) {
82
      prefs.remove( key + (i + 1) );
83
    }
84
  }
85
}
186
A src/main/r/README.md
1
# R Scripts
2
3
These R scripts illustrate how R can be used within an application to perform calculations using variables. Authors are free to write their own scripts, of course. These scripts serve as an example of how to automate certain tasks while writing.
4
5
## Configuration
6
7
Configure the editor to use the R scripts as follows:
8
9
1. Copy the R scripts into same directory as your Markdown files.
10
1. Start the editor.
11
1. Click **Tools → R Script**.
12
1. Copy and paste the following:
13
14
        assign( 'anchor', as.Date( '$date.anchor$', format='%Y-%m-%d' ), envir = .GlobalEnv );
15
        setwd( '$application.r.working.directory$' );
16
        source( 'pluralize.R' );
17
        source( 'csv.R' );
18
        source( 'conversion.R' );
19
20
1. Click **File → New** to create a new file.
21
1. Click **File → Save As** to set a filename.
22
1. Set **Name** to: `variables.yaml`
23
1. Click **OK**.
24
1. Paste the following definitions:
25
26
        date:
27
          anchor: 2017-01-01
28
        editor:
29
          examples:
30
            season: 2017-09-02
31
            math:
32
              x: 1
33
              y: $editor.examples.math.x$ + 1
34
              z: $editor.examples.math.y$ + 1
35
            name:
36
              given: Josephene
37
38
1. Save and close the file.
39
1. Click **File → Open**
40
1. Change **Markdown Files** to **Definition Files**.
41
1. Select `variables.yaml`.
42
1. Click **Open**.
43
44
R functionality is configured.
45
46
## Definitions
47
48
The variables definitions within `variables.yaml` are available to R using the R syntax. An additional variable, `application.r.working.directory` is added to the list of variables. The value is set to the working directory of the file being edited. Hover the mouse cursor over the file tab in the editor to see the full path to the file.
49
50
## Examples
51
52
This section demonstrates how to use the R functions when editing. Complete the following steps to begin:
53
54
1. Click **File → New** to create a new file.
55
1. Click **File → Save As** to set a filename.
56
1. Set **Name** to: `example.Rmd`
57
1. Click **OK**.
58
59
The examples are ready for use within the editor.
60
61
### Arithmetic
62
63
Type the following to perform a simple calculation:
64
65
    `r# 1+1`
66
67
The preview pane shows `2.0`.
68
69
### Functions
70
71
Call the [format](https://stat.ethz.ch/R-manual/R-devel/library/base/html/format.html) function to truncate unwanted decimal places as follows:
72
73
    `r# format(1+1,digits=1)`
74
75
The preview pane shows `2`.
76
77
### Pluralize
78
79
Many English words can be pluralized as follows:
80
81
    `r# pl('wolf',2)`
82
83
The preview pane shows `wolves`. The `pluralize.R` file contains a partial implementation of Damian Conway's algorithmic approach to English pluralization.
84
85
### Chicago Manual of Style
86
87
Apply the Chicago Manual of Style for words less than one-hundred as follows:
88
89
       `r# cms(1)` `r# cms(99)` `r# cms(101)`
90
91
The preview pane shows numbers written out as `one` and `ninety-nine`, followed by the digits 101.
92
93
### Data Import
94
95
Import and display information from a CSV file as follows:
96
97
1. Click **File → New** to create a new file.
98
1. Click **File → Save As** to rename the file.
99
1. Set the filename to: `data.csv`
100
1. Paste the following into `data.csv`:
101
102
        Animal,Quantity,Country
103
        Aardwolf,1,Africa
104
        Keel-billed toucan,1,Belize
105
        Beaver,2,Canada
106
        Mute swan,3,Denmark
107
        Lion,5,Ethiopia
108
        Brown bear,8,Finland
109
        Dolphin,13,Greece
110
        Turul,21,Hungary
111
        Gyrfalcon,34,Iceland
112
        Red-billed streamertail,55,Jamaica
113
114
1. Click the `example.Rmd` tab.
115
1. Type the following:
116
117
       `r# csv2md('data.csv',total=F)`
118
119
1. Type the following to calculate a total for all numeric columns:
120
121
       `r# csv2md('data.csv')`
122
123
This imports the data from an external file and formats the information into a table, automatically. Update the data as follows:
124
125
1. Click the `data.csv` tab to edit the data.
126
1. Change the data by adding a new row.
127
1. Save the file.
128
1. Click the `example.Rmd` tab.
129
130
The preview pane shows the revised contents.
131
132
### Elapsed Time
133
134
The duration of a timeline, given in numbers of days, can be computed into English as follows:
135
136
    `r# elapsed(1,1)`
137
138
The preview pane shows `same day`. Change the expression to:
139
140
    `r# elapsed(1,2)`
141
142
The preview pane shows `one day`. Change the expression to:
143
144
    `r# elapsed(1,112358)`
145
146
The preview pane shows `307 years, seven months, and sixteen days`, combined using the Chicago Manual of Style, the pluralization function, and a [serial comma](https://www.behance.net/gallery/19417363/The-Oxford-Comma).
147
148
### Variable Syntax
149
150
The syntax for a variable changes when using an R Markdown file (denoted by the `.Rmd` filename extension), as opposed to a regular Markdown file (`.md`). Return to the example file and type the following:
151
152
    `r# v$date$anchor`
153
154
The preview pane shows the date.
155
156
### Autocomplete
157
158
Automatically insert a variable reference into the text as follows:
159
160
1. Type: `Jos`
161
    * Note the capital letter, matches are case sensitive.
162
1. Hold down the `Control` key.
163
1. Tap the `Spacebar`
164
165
The editor shows:
166
167
    `r#x( v$editor$examples$name$given )`
168
169
The preview pane shows:
170
171
    Josephine
172
173
Here, the `x` function evaluates its parameter as an expression. This allows variables to include expressions in their definition.
174
175
### Variable Definition Expressions
176
177
Definition file variables are have the ability to reference other definitions. Try the following:
178
179
    x = `r#x( v$editor$examples$math$x )`;
180
    y = `r#x( v$editor$examples$math$y )`;
181
    z = `r#x( v$editor$examples$math$z )`
182
183
The preview pane shows:
184
185
    x = 1.0; y = 2.0; z = 3.0
186
187
### Case
188
189
Ensure words begin with a lowercase letter as follows:
190
191
    `r#lc( v$editor$examples$name$given )`
192
193
The preview pane shows:
194
195
    josephine
196
197
Similarly, ensure an uppercase letter as follows:
198
199
    `r#uc( 'hello, world!' )`
200
201
The preview pane shows:
202
203
    Hello, world!
204
205
### Month
206
207
Display the month name given a month number as follows:
208
209
    `r# month( 1 )`
210
211
The preview pane shows:
212
213
    January
214
215
## Summary
216
217
Authors can inline R statements into documents, directly, so long as those statements generate text. Plots, graphs, and images must be referenced as external image files or URLs.
1218
A src/main/r/conversion.R
1
# ########################################################################
2
#
3
# Substitute R expressions in a document with their evaluated value. The
4
# anchor variable must be set for functions that use relative dates.
5
#
6
# ########################################################################
7
8
# Evaluates an expression; writes s if there is no expression.
9
x <- function( s ) {
10
  return(
11
    tryCatch({
12
      r = eval( parse( text=s ) )
13
14
      # If the result isn't primitive, then it was probably parsed into
15
      # an unprintable object (e.g., "gray" becomes a colour). In those
16
      # cases, return the original text string. Otherwise, an atomic
17
      # value means a primitive type (string, integer, etc.) that can be
18
      # written directly into the document.
19
      #
20
      # See: http://stackoverflow.com/a/19501276/59087
21
      if( is.atomic( r ) ) {
22
        r
23
      }
24
      else {
25
        s
26
      }
27
    },
28
    warning = function( w ) {
29
      s
30
    },
31
    error = function( e ) {
32
      s
33
    })
34
  )
35
}
36
37
# Returns a date offset by a given number of days, relative to the given
38
# date (d). This does not use the anchor, but is used to get the anchor's
39
# value as a date.
40
when <- function( d, n = 0, format = "%Y-%m-%d" ) {
41
  as.Date( d, format = format ) + x( n )
42
}
43
44
# Full date (s) offset by an optional number of days before or after.
45
# This will remove leading zeros (applying leading spaces instead, which
46
# are ignored by any worthwhile typesetting engine).
47
annal <- function( days = 0, format = "%Y-%m-%d", oformat = "%B %d, %Y" ) {
48
  format( when( anchor, days ), format = oformat )
49
}
50
51
# Extracts the year from a date string.
52
year <- function( days = 0, format = "%Y-%m-%d" ) {
53
  annal( days, format, "%Y" )
54
}
55
56
# Day of the week (in days since the anchor date).
57
weekday <- function( n ) {
58
  weekdays( when( anchor, n ) )
59
}
60
61
# String concatenate function alias because paste0 is a terrible name.
62
concat <- paste0
63
64
# Translates a number from digits to words using Chicago Manual of Style.
65
# This does not translate numbers greater than one hundred. If ordinal
66
# is TRUE, this will return the ordinal name. This will not produce ordinals
67
# for numbers greater than 100
68
cms <- function( n, ordinal = FALSE ) {
69
  n <- x( n )
70
71
  # We're done here.
72
  if( n == 0 ) {
73
    if( ordinal ) {
74
      return( "zeroth" )
75
    }
76
77
    return( "zero" )
78
  }
79
80
  # Concatenate this a little later.
81
  if( n < 0 ) {
82
    result = "negative "
83
    n = abs( n )
84
  }
85
86
  # Do not spell out numbers greater than one hundred.
87
  if( n > 100 ) {
88
    # Comma-separated numbers.
89
    return( format( n, big.mark=",", trim=TRUE, scientific=FALSE ) )
90
  }
91
92
  # Don't go beyond 100.
93
  if( n == 100 ) {
94
    if( ordinal ) {
95
      return( "one hundredth" )
96
    }
97
98
    return( "one hundred" )
99
  }
100
101
  # Samuel Langhorne Clemens noted English has too many exceptions.
102
  small = c(
103
    "one", "two", "three", "four", "five",
104
    "six", "seven", "eight", "nine", "ten",
105
    "eleven", "twelve", "thirteen", "fourteen", "fifteen",
106
    "sixteen", "seventeen", "eighteen", "nineteen"
107
  )
108
109
  ord_small = c(
110
    "first", "second", "third", "fourth", "fifth",
111
    "sixth", "seventh", "eighth", "ninth", "tenth",
112
    "eleventh", "twelfth", "thirteenth", "fourteenth", "fifteenth",
113
    "sixteenth", "seventeenth", "eighteenth", "nineteenth", "twentieth"
114
  )
115
116
  # After this, the number (n) is between 20 and 99.
117
  if( n < 20 ) {
118
    if( ordinal ) {
119
      return( .subset( ord_small, n %% 100 ) )
120
    }
121
122
    return( .subset( small, n %% 100 ) )
123
  }
124
125
  tens = c( "",
126
    "twenty", "thirty", "forty", "fifty",
127
    "sixty", "seventy", "eighty", "ninety"
128
  )
129
130
  ord_tens = c( "",
131
    "twentieth", "thirtieth", "fortieth", "fiftieth",
132
    "sixtieth", "seventieth", "eightieth", "ninetieth"
133
  )
134
135
  ones_index = n %% 10
136
  n = n %/% 10
137
138
  # No number in the ones column, so the number must be a multiple of ten.
139
  if( ones_index == 0 ) {
140
    if( ordinal ) {
141
      return( .subset( ord_tens, n ) )
142
    }
143
144
    return( .subset( tens, n ) )
145
  }
146
147
  # Find the value from the ones column.
148
  if( ordinal ) {
149
    unit_1 = .subset( ord_small, ones_index )
150
  }
151
  else {
152
    unit_1 = .subset( small, ones_index )
153
  }
154
155
  # Find the tens column.
156
  unit_10 = .subset( tens, n )
157
158
  # Hyphenate the tens and the ones together.
159
  concat( unit_10, concat( "-", unit_1 ) )
160
}
161
162
# Returns a human-readable string that provides the elapsed time between
163
# two numbers in terms of years, months, and days. If any unit value is zero,
164
# the unit is not included. The words (year, month, day) are pluralized
165
# according to English grammar. The numbers are written out according to
166
# Chicago Manual of Style. This applies the serial comma.
167
#
168
# Both numbers are offsets relative to the anchor date.
169
#
170
# If all unit values are zero, this returns s ("same day" by default).
171
#
172
# If the start date (began) is greater than end date (ended), the dates are
173
# swapped before calculations are performed. This allows any two dates
174
# to be compared and positive unit values are always returned.
175
#
176
elapsed <- function( began, ended, s = "same day" ) {
177
  began = when( anchor, began )
178
  ended = when( anchor, ended )
179
180
  # Swap the dates if the end date comes before the start date.
181
  if( as.integer( ended - began ) < 0 ) {
182
    tempd = began
183
    began = ended
184
    ended = tempd
185
  }
186
187
  # Calculate number of elapsed years.
188
  years = length( seq( from = began, to = ended, by = 'year' ) ) - 1
189
190
  # Move the start date up by the number of elapsed years.
191
  if( years > 0 ) {
192
    began = seq( began, length = 2, by = concat( years, " years" ) )[2]
193
    years = pl.numeric( "year", years )
194
  }
195
  else {
196
    # Zero years.
197
    years = ""
198
  }
199
200
  # Calculate number of elapsed months, excluding years.
201
  months = length( seq( from = began, to = ended, by = 'month' ) ) - 1
202
203
  # Move the start date up by the number of elapsed months
204
  if( months > 0 ) {
205
    began = seq( began, length = 2, by = concat( months, " months" ) )[2]
206
    months = pl.numeric( "month", months )
207
  }
208
  else {
209
    # Zero months
210
    months = ""
211
  }
212
213
  # Calculate number of elapsed days, excluding months and years.
214
  days = length( seq( from = began, to = ended, by = 'day' ) ) - 1
215
216
  if( days > 0 ) {
217
    days = pl.numeric( "day", days )
218
  }
219
  else {
220
    # Zero days
221
    days = ""
222
  }
223
224
  if( years <= 0 && months <= 0 && days <= 0 ) {
225
    return( s )
226
  }
227
228
  # Put them all in a vector, then remove the empty values.
229
  s <- c( years, months, days )
230
  s <- s[ s != "" ]
231
232
  r <- paste( s, collapse = ", " )
233
234
  # If all three items are present, replace the last comma with ", and".
235
  if( length( s ) > 2 ) {
236
    return( gsub( "(.*),", "\\1, and", r ) )
237
  }
238
239
  # Does nothing if no commas are present.
240
  gsub( "(.*),", "\\1 and", r )
241
}
242
243
# Returns the number (n) in English followed by the plural or singular
244
# form of the given string (s; resumably a noun), if applicable, according
245
# to English grammar. That is, pl.numeric( "wolf", 5 ) will return
246
# "five wolves".
247
pl.numeric <- function( s, n ) {
248
  concat( cms( n ), concat( " ", pluralize( s, n ) ) )
249
}
250
251
# Name of the season, starting with an capital letter.
252
season <- function( n, format = "%Y-%m-%d" ) {
253
  WS <- as.Date("2016-12-15", "%Y-%m-%d") # Winter Solstice
254
  SE <- as.Date("2016-03-15", "%Y-%m-%d") # Spring Equinox
255
  SS <- as.Date("2016-06-15", "%Y-%m-%d") # Summer Solstice
256
  AE <- as.Date("2016-09-15", "%Y-%m-%d") # Autumn Equinox
257
258
  d <- when( anchor, n )
259
  d <- as.Date( strftime( d, format="2016-%m-%d" ) )
260
261
  ifelse( d >= WS | d < SE, "Winter",
262
    ifelse( d >= SE & d < SS, "Spring",
263
      ifelse( d >= SS & d < AE, "Summer", "Autumn" )
264
    )
265
  )
266
}
267
268
# Converts the first letter in a string to lowercase
269
lc <- function( s ) {
270
  concat( tolower( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
271
}
272
273
# Converts the first letter in a string to uppercase
274
uc <- function( s ) {
275
  concat( toupper( substr( s, 1, 1 ) ), substr( s, 2, nchar( s ) ) )
276
}
277
278
# Returns the number of days between the given dates.
279
days <- function( d1, d2, format = "%Y-%m-%d" ) {
280
  dates = c( d1, d2 )
281
  dt = strptime( dates, format = format )
282
  as.integer( difftime( dates[2], dates[1], units = "days" ) )
283
}
284
285
# Returns the number of years elapsed.
286
years <- function( began, ended ) {
287
  began = when( anchor, began )
288
  ended = when( anchor, ended )
289
290
  # Swap the dates if the end date comes before the start date.
291
  if( as.integer( ended - began ) < 0 ) {
292
    tempd = began
293
    began = ended
294
    ended = tempd
295
  }
296
297
  # Calculate number of elapsed years.
298
  length( seq( from = began, to = ended, by = 'year' ) ) - 1
299
}
300
301
# Full name of the month, starting with a capital letter.
302
month <- function( n ) {
303
  # Faster than month.name[ x( n ) ]
304
  .subset( month.name, x( n ) )
305
}
306
307
money <- function( n ) {
308
  formatC( x( n ), format="d" )
309
}
1310
A src/main/r/csv.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# Converts CSV to Markdown.
29
#
30
# ######################################################################
31
32
# Reads a CSV file and converts the contents to a Markdown table. The
33
# file must be in the working directory as specified by setwd.
34
#
35
# @param f The filename to convert.
36
# @param decimals Rounded decimal places (default 1).
37
# @param totals Include total sums (default TRUE).
38
# @param align Right-align numbers (default TRUE).
39
csv2md <- function( f, decimals = 1, totals = T, align = T ) {
40
  # Read the CVS data from the file; ensure strings become characters.
41
  df <- read.table( f, sep=',', header=T, stringsAsFactors=F )
42
43
  if( totals ) {
44
    # Determine what columns can be summed.
45
    number <- which( unlist( lapply( df, is.numeric ) ) )
46
47
    # Use colSums when more than one summable column exists.
48
    if( length( number ) > 1 ) {
49
      f.sum <- colSums
50
    }
51
    else {
52
      f.sum <- sum
53
    }
54
55
    # Calculate the sum of all the summable columns and insert the
56
    # results back into the data frame.
57
    df[ (nrow( df ) + 1), number ] <- f.sum( df[, number], na.rm=TRUE )
58
59
    # pluralize would be heavyweight here.
60
    if( length( number ) > 1 ) {
61
      t <- "**Totals**"
62
    }
63
    else {
64
      t <- "**Total**"
65
    }
66
67
    # Change the first column of the last line to "Total(s)".
68
    df[ nrow( df ), 1 ] <- t
69
70
    # Don't clutter the output with "NA" text.
71
    df[ is.na( df ) ] <- ""
72
  }
73
74
  if( align ) {
75
    is.char <- vapply( df, is.character, logical( 1 ) )
76
    dashes <- paste( ifelse( is.char, ':---', '---:' ), collapse='|' )
77
  }
78
  else {
79
    dashes <- paste( rep( '---', length( df ) ), collapse = '|')
80
  }
81
82
  # Create a Markdown version of the data frame.
83
  paste(
84
    paste( names( df ), collapse = '|'), '\n',
85
    dashes, '\n', 
86
    paste(
87
      Reduce( function( x, y ) {
88
          paste( x, format( y, digits = decimals ), sep = '|' )
89
        }, df
90
      ),
91
      collapse = '|\n', sep=''
92
    )
93
  )
94
}
95
196
A src/main/r/pluralize.R
1
# ######################################################################
2
#
3
# Copyright 2016, White Magic Software, Ltd.
4
# 
5
# Permission is hereby granted, free of charge, to any person obtaining
6
# a copy of this software and associated documentation files (the
7
# "Software"), to deal in the Software without restriction, including
8
# without limitation the rights to use, copy, modify, merge, publish,
9
# distribute, sublicense, and/or sell copies of the Software, and to
10
# permit persons to whom the Software is furnished to do so, subject to
11
# the following conditions:
12
# 
13
# The above copyright notice and this permission notice shall be
14
# included in all copies or substantial portions of the Software.
15
# 
16
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
#
24
# ######################################################################
25
26
# ######################################################################
27
#
28
# See Damian Conway's "An Algorithmic Approach to English Pluralization":
29
#   http://goo.gl/oRL4MP
30
# See Oliver Glerke's Evo Inflector: https://github.com/atteo/evo-inflector/
31
# See Shevek's Pluralizer: https://github.com/shevek/linguistics/
32
# See also: http://www.freevectors.net/assets/files/plural.txt
33
#
34
# ######################################################################
35
36
pluralize <- function( s, n ) {
37
  result <- s
38
39
  # Partial implementation of Conway's algorithm for nouns.
40
  if( n != 1 ) {
41
    if( pl.noninflective( s ) ||
42
        pl.suffix( "fish", s ) ||
43
        pl.suffix( "ois", s ) ||
44
        pl.suffix( "sheep", s ) ||
45
        pl.suffix( "deer", s ) ||
46
        pl.suffix( "pox", s ) ||
47
        pl.suffix( "[A-Z].*ese", s ) ||
48
        pl.suffix( "itis", s ) ) {
49
      # 1. Retain non-inflective user-mapped noun as is.
50
      # 2. Retain non-inflective plural as is.
51
      result <- s
52
    }
53
    else if( pl.is.irregular.pl( s ) ) {
54
      # 4. Change irregular plurals based on mapping.
55
      result <- pl.irregular.pl( s )
56
    }
57
    else if( pl.is.irregular.es( s ) ) {
58
      # x. From Shevek's Pluralizer
59
      result <- pl.inflect( s, "", "es" )
60
    }
61
    else if( pl.suffix( "man", s ) ) {
62
      # 5. For -man, change -an to -en
63
      result <- pl.inflect( s, "an", "en" )
64
    }
65
    else if( pl.suffix( "[lm]ouse", s ) ) {
66
      # 5. For [lm]ouse, change -ouse to -ice
67
      result <- pl.inflect( s, "ouse", "ice" )
68
    }
69
    else if( pl.suffix( "tooth", s ) ) {
70
      # 5. For -tooth, change -ooth to -eeth
71
      result <- pl.inflect( s, "ooth", "eeth" )
72
    }
73
    else if( pl.suffix( "goose", s ) ) {
74
      # 5. For -goose, change -oose to -eese
75
      result <- pl.inflect( s, "oose", "eese" )
76
    }
77
    else if( pl.suffix( "foot", s ) ) {
78
      # 5. For -foot, change -oot to -eet
79
      result <- pl.inflect( s, "oot", "eet" )
80
    }
81
    else if( pl.suffix( "zoon", s ) ) {
82
      # 5. For -zoon, change -on to -a
83
      result <- pl.inflect( s, "on", "a" )
84
    }
85
    else if( pl.suffix( "[csx]is", s ) ) {
86
      # 5. Change -cis, -sis, -xis to -es
87
      result <- pl.inflect( s, "is", "es" )
88
    }
89
    else if( pl.suffix( "([cs]h|ss)", s ) ) {
90
      # 8. Change -ch, -sh, -ss to -es
91
      result <- pl.inflect( s, "", "es" )
92
    }
93
    else if( pl.suffix( "([aeo]lf|[^d]eaf|arf)", s ) ) {
94
      # 9. Change -f to -ves
95
      result <- pl.inflect( s, "f", "ves" )
96
    }
97
    else if( pl.suffix( "[nlw]ife", s ) ) {
98
      # 9. Change -fe to -ves
99
      result <- pl.inflect( s, "fe", "ves" )
100
    }
101
    else if( pl.suffix( "([aeiou]y|[A-Z].*y)", s ) ) {
102
      # 10. Change -y to -ys.
103
      result <- pl.inflect( s, "", "s" )
104
    }
105
    else if( pl.suffix( "y", s ) ) {
106
      # 10. Change -y to -ies.
107
      result <- pl.inflect( s, "y", "ies" )
108
    }
109
    else {
110
      # 13. Default plural: add -s.
111
      result <- pl.inflect( s, "", "s" )
112
    }
113
  }
114
115
  result
116
}
117
118
# Pluralize s if n is not equal to 1.
119
pl <- function( s, n ) {
120
  pluralize( s, x( n ) )
121
}
122
123
# Returns the given string (s) with its suffix replaced by r.
124
pl.inflect <- function( s, suffix, r ) {
125
  gsub( paste( suffix, "$", sep="" ), r, s )
126
}
127
128
# Answers whether the given string (s) has the given ending.
129
pl.suffix <- function( ending, s ) {
130
  grepl( paste( ending, "$", sep="" ), s )
131
}
132
133
# Answers whether the given string (s) is a noninflective noun.
134
pl.noninflective <- function( s ) {
135
  v <- c(
136
    "aircraft", "Bhutanese", "bison", "bream", "breeches", "britches",
137
    "Burmese", "carp", "chassis", "Chinese", "clippers", "cod", "contretemps",
138
    "corps", "debris", "diabetes", "djinn", "eland", "elk", "flounder",
139
    "fracas", "gallows", "graffiti", "headquarters", "herpes", "high-jinks",
140
    "homework", "hovercraft", "innings", "jackanapes", "Japanese",
141
    "Lebanese", "mackerel", "means", "measles", "mews", "mumps", "news",
142
    "pincers", "pliers", "Portuguese", "proceedings", "rabies", "salmon",
143
    "scissors", "sea-bass", "Senegalese", "series", "shears", "Siamese",
144
    "Sinhalese", "spacecraft", "species", "swine", "trout", "tuna",
145
    "Vietnamese", "watercraft", "whiting", "wildebeest"
146
  )
147
148
  is.element( s, v )
149
}
150
151
# Answers whether the given string (s) is an irregular plural.
152
pl.is.irregular.pl <- function( s ) {
153
  # Could be refactored with pl.irregular.pl...
154
  v <- c(
155
    "beef", "brother", "child", "cow", "ephemeris", "genie", "money",
156
    "mongoose", "mythos", "octopus", "ox", "soliloquy", "trilby"
157
  )
158
159
  is.element( s, v )
160
}
161
162
# Call to pluralize an irregular noun. Only call after confirming
163
# the noun is irregular via pl.is.irregular.pl.
164
pl.irregular.pl <- function( s ) {
165
  v <- list(
166
    "beef" = "beefs",
167
    "brother" = "brothers",
168
    "child" = "children",
169
    "cow" = "cows",
170
    "ephemeris" = "ephemerides",
171
    "genie" = "genies",
172
    "money" = "moneys",
173
    "mongoose" = "mongooses",
174
    "mythos" = "mythoi",
175
    "octopus" = "octopuses",
176
    "ox" = "oxen",
177
    "soliloquy" = "soliloquies",
178
    "trilby" = "trilbys"
179
  )
180
181
  # Faster version of v[[ s ]]
182
  .subset2( v, s )
183
}
184
185
# Answers whether the given string (s) pluralizes with -es.
186
pl.is.irregular.es <- function( s ) {
187
  v <- c(
188
    "acropolis", "aegis", "alias", "asbestos", "bathos", "bias", "bronchitis",
189
    "bursitis", "caddis", "cannabis", "canvas", "chaos", "cosmos", "dais",
190
    "digitalis", "epidermis", "ethos", "eyas", "gas", "glottis", "hubris",
191
    "ibis", "lens", "mantis", "marquis", "metropolis", "pathos", "pelvis",
192
    "polis", "rhinoceros", "sassafrass", "trellis"
193
  )
194
195
  is.element( s, v )
196
}
197
1198
A src/main/resources/META-INF/services/com.scrivenvar.service.Options
1
1
com.scrivenvar.service.impl.DefaultOptions
A src/main/resources/META-INF/services/com.scrivenvar.service.Settings
1
1
com.scrivenvar.service.impl.DefaultSettings
A src/main/resources/META-INF/services/com.scrivenvar.service.Snitch
1
1
com.scrivenvar.service.impl.DefaultSnitch
A src/main/resources/META-INF/services/com.scrivenvar.service.events.Notifier
1
1
com.scrivenvar.service.events.impl.DefaultNotifier
A src/main/resources/com/scrivenvar/.gitignore
1
app.properties
12
A src/main/resources/com/scrivenvar/build.sh
1
#!/bin/bash
2
3
INKSCAPE="/usr/bin/inkscape"
4
PNG_COMPRESS="optipng"
5
PNG_COMPRESS_OPTS="-o9 *png"
6
ICO_TOOL="icotool"
7
ICO_TOOL_OPTS="-c -o ../../../../../icons/logo.ico logo64.png"
8
9
declare -a SIZES=("16" "32" "64" "128" "256" "512")
10
11
for i in "${SIZES[@]}"; do
12
  # -y: export background opacity 0
13
  $INKSCAPE -y 0 -z -f "logo.svg" -w "${i}" -e "logo${i}.png"
14
done
15
16
# Compess the PNG images.
17
which $PNG_COMPRESS && $PNG_COMPRESS $PNG_COMPRESS_OPTS
18
19
# Generate an ICO file.
20
which $ICO_TOOL && $ICO_TOOL $ICO_TOOL_OPTS
21
122
A src/main/resources/com/scrivenvar/editor/markdown.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
.markdown-editor {
29
  -fx-font-size: 14px;
30
}
31
32
/*---- headers ----*/
33
34
.markdown-editor .h1 { -fx-font-size: 2.25em; }
35
.markdown-editor .h2 { -fx-font-size: 1.75em; }
36
.markdown-editor .h3 { -fx-font-size: 1.5em; }
37
.markdown-editor .h4 { -fx-font-size: 1.25em; }
38
.markdown-editor .h5 { -fx-font-size: 1.1em; }
39
.markdown-editor .h6 { -fx-font-size: 1em; }
40
41
.markdown-editor .h1,
42
.markdown-editor .h2,
43
.markdown-editor .h3,
44
.markdown-editor .h4,
45
.markdown-editor .h5,
46
.markdown-editor .h6 {
47
  -fx-font-weight: bold;
48
  -fx-fill: derive(crimson, -20%);
49
}
50
51
52
/*---- inlines ----*/
53
54
.markdown-editor .strong {
55
  -fx-font-weight: bold;
56
}
57
58
.markdown-editor .em {
59
  -fx-font-style: italic;
60
}
61
62
.markdown-editor .del {
63
  -fx-strikethrough: true;
64
}
65
66
.markdown-editor .a {
67
  -fx-fill: #4183C4 !important;
68
}
69
70
.markdown-editor .img {
71
  -fx-fill: #4183C4 !important;
72
}
73
74
.markdown-editor .code {
75
  -fx-font-family: monospace;
76
  -fx-fill: #090 !important;
77
}
78
79
80
/*---- blocks ----*/
81
82
.markdown-editor .pre {
83
  -fx-font-family: monospace;
84
  -fx-fill: #060 !important;
85
}
86
87
.markdown-editor .blockquote {
88
  -fx-fill: #777;
89
}
90
91
92
/*---- lists ----*/
93
94
.markdown-editor .ul {
95
}
96
97
.markdown-editor .ol {
98
}
99
100
.markdown-editor .li {
101
  -fx-fill: #444;
102
}
103
104
.markdown-editor .dl {
105
}
106
107
.markdown-editor .dt {
108
  -fx-font-weight: bold;
109
  -fx-font-style: italic;
110
}
111
112
.markdown-editor .dd {
113
  -fx-fill: #444;
114
}
115
116
117
/*---- table ----*/
118
119
.markdown-editor .table {
120
  -fx-font-family: monospace;
121
}
122
123
.markdown-editor .thead {
124
}
125
126
.markdown-editor .tbody {
127
}
128
129
.markdown-editor .caption {
130
}
131
132
.markdown-editor .th {
133
  -fx-font-weight: bold;
134
}
135
136
.markdown-editor .tr {
137
}
138
139
.markdown-editor .td {
140
}
141
142
143
/*---- misc ----*/
144
145
.markdown-editor .html {
146
  -fx-font-family: monospace;
147
  -fx-fill: derive(crimson, -50%);
148
}
149
.markdown-editor .monospace {
150
  -fx-font-family: monospace;
151
}
1152
A src/main/resources/com/scrivenvar/logo.svg
1
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
2
<!-- Created with Inkscape (http://www.inkscape.org/) -->
3
4
<svg
5
   xmlns:dc="http://purl.org/dc/elements/1.1/"
6
   xmlns:cc="http://creativecommons.org/ns#"
7
   xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
8
   xmlns:svg="http://www.w3.org/2000/svg"
9
   xmlns="http://www.w3.org/2000/svg"
10
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
11
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
12
   id="svg2"
13
   version="1.1"
14
   inkscape:version="0.91 r13725"
15
   width="512"
16
   height="512"
17
   viewBox="0 0 512 512"
18
   sodipodi:docname="logo.svg">
19
  <metadata
20
     id="metadata8">
21
    <rdf:RDF>
22
      <cc:Work
23
         rdf:about="">
24
        <dc:format>image/svg+xml</dc:format>
25
        <dc:type
26
           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
27
        <dc:title></dc:title>
28
      </cc:Work>
29
    </rdf:RDF>
30
  </metadata>
31
  <defs
32
     id="defs6" />
33
  <sodipodi:namedview
34
     pagecolor="#ffffff"
35
     bordercolor="#666666"
36
     borderopacity="1"
37
     objecttolerance="10"
38
     gridtolerance="10"
39
     guidetolerance="10"
40
     inkscape:pageopacity="0"
41
     inkscape:pageshadow="2"
42
     inkscape:window-width="640"
43
     inkscape:window-height="480"
44
     id="namedview4"
45
     showgrid="false"
46
     fit-margin-top="0"
47
     fit-margin-left="0"
48
     fit-margin-right="0"
49
     fit-margin-bottom="0"
50
     inkscape:zoom="1.2682274"
51
     inkscape:cx="15.646213"
52
     inkscape:cy="213.34955"
53
     inkscape:current-layer="svg2" />
54
  <path
55
     style="fill:#ce6200;fill-opacity:1"
56
     d="m 203.2244,511.85078 c -60.01827,-1.2968 -121.688643,-6.5314 -192.436493,-16.334 -5.8078027,-0.8047 -10.66110747,-1.561 -10.78511762,-1.6806 -0.12404567,-0.1196 3.90488112,-4.5812 8.95313512,-9.9147 32.9484785,-34.8102 70.4314485,-73.8923 104.1521555,-108.5956 l 11.87611,-12.2221 5.48905,-10.2177 c 35.82801,-66.6927 75.13064,-128.5665 105.90637,-166.7277 6.13805,-7.611 10.21451,-12.0689 17.28719,-18.9048 36.6818,-35.4537 108.27279,-83.724003 206.0323,-138.917303 22.10365,-12.47935 51.93386,-28.64995037 52.26391,-28.33165037 0.38883,0.37499 -2.35932,25.95575037 -4.86585,45.29275037 -7.28943,56.236403 -17.04619,103.128903 -28.07642,134.939803 -7.19617,20.7536 -14.81287,35.152 -22.9667,43.4155 -3.60444,3.6529 -6.58328,5.7941 -10.1313,7.2825 l -2.56414,1.0756 -53.43164,0.1713 -53.43166,0.1713 3.69973,1.8547 c 26.78565,13.4282 52.58051,27.5241 59.57122,32.5533 4.48397,3.2259 4.41278,2.9854 1.59124,5.3784 -26.99514,22.8955 -74.52961,44.0013 -140.23089,62.2641 -26.34995,7.3244 -57.85469,14.6842 -86.99871,20.3237 l -10.26943,1.9871 -52.01052,53.2733 -52.010524,53.2732 -29.459801,15.1165 c -26.4100885,13.5517 -29.3446639,15.1388 -28.347645,15.3311 0.6117029,0.118 4.0894221,0.2188 7.7282726,0.2239 3.6388854,0.01 16.1273694,0.2329 27.7522124,0.5059 51.576376,1.2116 146.083985,1.512 170.154295,0.5409 34.66996,-1.3988 52.7606,-2.9325 67.58258,-5.7293 2.68664,-0.507 4.82907,-0.9755 4.76094,-1.0412 -0.0681,-0.066 -3.24733,-0.8833 -7.0649,-1.8169 -8.04133,-1.9664 -25.10167,-5.3107 -41.1231,-8.0612 -47.6405,-8.1787 -65.48708,-12.0107 -74.13028,-15.9169 -3.90548,-1.7651 -7.13816,-4.7659 -8.12937,-7.5463 -1.01822,-2.8562 -0.92214,-6.5271 0.23315,-8.9083 1.86563,-3.8451 6.14837,-6.7199 12.26745,-8.2345 16.96993,-4.2004 57.27977,-6.1832 90.36228,-4.4448 54.7332,2.8761 117.0767,13.1228 178.50212,29.3385 18.03514,4.7611 51.66065,14.656 51.22677,15.0744 -0.0824,0.08 -5.72762,-0.854 -12.54488,-2.0745 -40.1043,-7.18 -60.50854,-10.2888 -101.40822,-15.4507 -24.4851,-3.0902 -55.12614,-5.9915 -77.58876,-7.3465 -26.58826,-1.6039 -61.15821,-1.7754 -80.99202,-0.4019 l -3.19705,0.2214 8.70308,1.4934 c 51.89698,8.9047 77.51746,14.9877 88.00479,20.8948 6.9134,3.894 10.30497,9.4381 9.33333,15.2569 -1.50397,9.0066 -10.51381,14.0257 -32.00273,17.8278 -16.31374,2.8863 -47.27575,4.3845 -77.23553,3.7371 z"
57
     id="path4138" />
58
  <path
59
     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
60
     d="m 214.76931,324.51908 c 60.83777,-14.1145 111.89562,-31.6251 144.40025,-49.5229 3.12602,-1.7213 5.81747,-3.2537 5.98106,-3.4054 0.40534,-0.3759 -13.76388,-7.9415 -34.63489,-18.4929 -7.52161,-3.8026 -9.82337,-5.3787 -12.0735,-8.2668 -5.14485,-6.6036 -5.96081,-14.8404 -2.20331,-22.2417 1.80288,-3.5512 5.69484,-7.3007 9.36158,-9.019 5.20851,-2.4407 1.18148,-2.2865 59.71223,-2.2865 l 52.81361,0 2.13233,-2.1984 c 2.78673,-2.8731 5.23414,-6.4981 8.23035,-12.1905 14.14966,-26.8827 26.71842,-78.3816 36.24347,-148.503303 0.76704,-5.6468 1.36194,-10.2983 1.32201,-10.3369 -0.0399,-0.038 -5.47754,2.9629 -12.08361,6.6697 l -12.01104,6.7396 -133.83068,137.037303 c -73.60688,75.3705 -134.81732,138.0567 -136.0232,139.3026 l -2.19251,2.2653 8.254,-1.8067 c 4.53969,-0.9937 12.01053,-2.6783 16.60185,-3.7435 z"
61
     id="path4136" />
62
  <path
63
     style="fill:#ffffff;fill-opacity:1;stroke:none;stroke-opacity:1"
64
     d="m 202.72524,284.43588 c 69.93294,-70.1332 135.4799,-131.9279 213.46406,-201.244203 7.71421,-6.8568 14.50542,-12.9341 15.09155,-13.5052 0.9482,-0.9239 0.96778,-0.9811 0.17761,-0.5188 -77.96496,45.611803 -139.23519,88.710503 -166.72539,117.278203 -18.81811,19.5556 -50.35654,64.861 -80.96704,116.3104 -0.91787,1.5427 1.02249,-0.3323 18.95921,-18.3204 z"
65
     id="path4142" />
66
  <path
67
     style="fill:#000000"
68
     d=""
69
     id="path4140"
70
     inkscape:connector-curvature="0" />
71
</svg>
172
A src/main/resources/com/scrivenvar/logo128.png
Binary file
A src/main/resources/com/scrivenvar/logo16.png
Binary file
A src/main/resources/com/scrivenvar/logo256.png
Binary file
A src/main/resources/com/scrivenvar/logo32.png
Binary file
A src/main/resources/com/scrivenvar/logo512.png
Binary file
A src/main/resources/com/scrivenvar/logo64.png
Binary file
A src/main/resources/com/scrivenvar/messages.properties
1
# ########################################################################
2
# Main Application Window
3
# ########################################################################
4
5
# The application title should exist only once in the entire code base.
6
# All other references should either refer to this value via the Messages
7
# class, or indirectly using ${Main.title}.
8
Main.title=Scrivenvar
9
10
Main.menu.file=_File
11
Main.menu.file.new=_New
12
Main.menu.file.open=_Open...
13
Main.menu.file.close=_Close
14
Main.menu.file.close_all=Close All
15
Main.menu.file.save=_Save
16
Main.menu.file.save_as=Save _As
17
Main.menu.file.save_all=Save A_ll
18
Main.menu.file.exit=E_xit
19
20
Main.menu.edit=_Edit
21
Main.menu.edit.undo=_Undo
22
Main.menu.edit.redo=_Redo
23
Main.menu.edit.find=_Find
24
Main.menu.edit.find.next=Find _Next
25
Main.menu.edit.preferences=_Preferences
26
27
Main.menu.insert=_Insert
28
Main.menu.insert.bold=Bold
29
Main.menu.insert.italic=Italic
30
Main.menu.insert.superscript=Superscript
31
Main.menu.insert.subscript=Subscript
32
Main.menu.insert.strikethrough=Strikethrough
33
Main.menu.insert.blockquote=Blockquote
34
Main.menu.insert.code=Inline Code
35
Main.menu.insert.fenced_code_block=Fenced Code Block
36
Main.menu.insert.fenced_code_block.prompt=Enter code here
37
Main.menu.insert.link=Link...
38
Main.menu.insert.image=Image...
39
Main.menu.insert.header.1=Header 1
40
Main.menu.insert.header.1.prompt=header 1
41
Main.menu.insert.header.2=Header 2
42
Main.menu.insert.header.2.prompt=header 2
43
Main.menu.insert.header.3=Header 3
44
Main.menu.insert.header.3.prompt=header 3
45
Main.menu.insert.unordered_list=Unordered List
46
Main.menu.insert.ordered_list=Ordered List
47
Main.menu.insert.horizontal_rule=Horizontal Rule
48
49
Main.menu.help=_Help
50
Main.menu.help.about=About ${Main.title}
51
52
# ########################################################################
53
# Status Bar
54
# ########################################################################
55
56
Main.statusbar.text.offset=offset
57
Main.statusbar.line=Line {0} of {1}, ${Main.statusbar.text.offset} {2}
58
Main.statusbar.state.default=OK
59
Main.statusbar.parse.error={0} (near ${Main.statusbar.text.offset} {1})
60
61
# ########################################################################
62
# Preferences
63
# ########################################################################
64
65
Preferences.r=R
66
Preferences.r.script=Startup Script
67
Preferences.r.script.desc=Script runs prior to executing R statements within the document.
68
Preferences.r.directory=Working Directory
69
Preferences.r.directory.desc=Value assigned to $application.r.working.directory$ and usable in the startup script.
70
71
Preferences.images=Images
72
Preferences.images.directory=Relative Directory
73
Preferences.images.directory.desc=Path prepended to embedded images referenced using local file paths.
74
Preferences.images.suffixes=Extensions
75
Preferences.images.suffixes.desc=Preferred order of image file types to embed, separated by spaces.
76
77
Preferences.definitions=Definitions
78
Preferences.definitions.path=File name
79
Preferences.definitions.path.desc=Absolute path to interpolated string definitions.
80
81
# ########################################################################
82
# Definition Pane and its Tree View
83
# ########################################################################
84
85
Definition.menu.create=Create
86
Definition.menu.rename=Rename
87
Definition.menu.remove=Delete
88
Definition.menu.add.default=Undefined
89
90
# ########################################################################
91
# Failure messages with respect to YAML files.
92
# ########################################################################
93
yaml.error.open=Could not open YAML file (ensure non-empty file).
94
yaml.error.unresolvable=Too much indirection for: ''{0}'' = ''{1}''.
95
yaml.error.missing=Empty definition value for key ''{0}''.
96
yaml.error.tree.form=Unassigned definition near ''{0}''.
97
98
# ########################################################################
99
# File Editor
100
# ########################################################################
101
102
FileEditor.loadFailed.message=Failed to load ''{0}''.\n\nReason: {1}
103
FileEditor.loadFailed.title=Load
104
FileEditor.loadFailed.reason.permissions=File must be readable and writable.
105
FileEditor.saveFailed.message=Failed to save ''{0}''.\n\nReason: {1}
106
FileEditor.saveFailed.title=Save
107
108
# ########################################################################
109
# File Open
110
# ########################################################################
111
112
Dialog.file.choose.open.title=Open File
113
Dialog.file.choose.save.title=Save File
114
115
Dialog.file.choose.filter.title.source=Source Files
116
Dialog.file.choose.filter.title.definition=Definition Files
117
Dialog.file.choose.filter.title.xml=XML Files
118
Dialog.file.choose.filter.title.all=All Files
119
120
# ########################################################################
121
# Alert Dialog
122
# ########################################################################
123
124
Alert.file.close.title=Close
125
Alert.file.close.text=Save changes to {0}?
126
127
# ########################################################################
128
# Definition Pane
129
# ########################################################################
130
131
Pane.definition.node.root.title=Definitions
132
133
# Controls ###############################################################
134
135
# ########################################################################
136
# Browse Directory
137
# ########################################################################
138
139
BrowseDirectoryButton.chooser.title=Browse for local folder
140
BrowseDirectoryButton.tooltip=${BrowseDirectoryButton.chooser.title}
141
142
# ########################################################################
143
# Browse File
144
# ########################################################################
145
146
BrowseFileButton.chooser.title=Browse for local file
147
BrowseFileButton.chooser.allFilesFilter=All Files
148
BrowseFileButton.tooltip=${BrowseFileButton.chooser.title}
149
150
# Dialogs ################################################################
151
152
# ########################################################################
153
# Image
154
# ########################################################################
155
156
Dialog.image.title=Image
157
Dialog.image.chooser.imagesFilter=Images
158
Dialog.image.previewLabel.text=Markdown Preview\:
159
Dialog.image.textLabel.text=Alternate Text\:
160
Dialog.image.titleLabel.text=Title (tooltip)\:
161
Dialog.image.urlLabel.text=Image URL\:
162
163
# ########################################################################
164
# Hyperlink
165
# ########################################################################
166
167
Dialog.link.title=Link
168
Dialog.link.previewLabel.text=Markdown Preview\:
169
Dialog.link.textLabel.text=Link Text\:
170
Dialog.link.titleLabel.text=Title (tooltip)\:
171
Dialog.link.urlLabel.text=Link URL\:
172
173
# ########################################################################
174
# About
175
# ########################################################################
176
177
Dialog.about.title=About
178
Dialog.about.header=${Main.title}
179
Dialog.about.content=Copyright 2020 White Magic Software, Ltd.\n\nBased on Markdown Writer FX by Karl Tauber
180
181
# Options ################################################################
182
183
# ########################################################################
184
# Options Dialog
185
# ########################################################################
186
187
OptionsDialog.title=Options
188
OptionsDialog.generalTab.text=General
189
OptionsDialog.markdownTab.text=Markdown
190
191
# ########################################################################
192
# General Options Pane
193
# ########################################################################
194
195
GeneralOptionsPane.encodingLabel.text=En_coding\:
196
GeneralOptionsPane.lineSeparatorLabel.text=_Line separator\:
197
GeneralOptionsPane.lineSeparatorLabel2.text=(applies to new files only)
198
199
GeneralOptionsPane.platformDefault=Platform Default ({0})
200
GeneralOptionsPane.sepWindows=Windows (CRLF)
201
GeneralOptionsPane.sepUnix=Unix (LF)
202
203
# ########################################################################
204
# Markdown Options Pane
205
# ########################################################################
206
207
MarkdownOptionsPane.abbreviationsExtCheckBox.text=A_bbreviations in the way of
208
MarkdownOptionsPane.abbreviationsExtLink.text=Markdown Extra
209
MarkdownOptionsPane.anchorlinksExtCheckBox.text=_Anchor links in headers
210
MarkdownOptionsPane.atxHeaderSpaceExtCheckBox.text=Requires a space char after Atx \# header prefixes, so that \#dasdsdaf is not a header
211
MarkdownOptionsPane.autolinksExtCheckBox.text=_Plain (undelimited) autolinks in the way of
212
MarkdownOptionsPane.autolinksExtLink.text=Github-flavoured-Markdown
213
MarkdownOptionsPane.definitionListsExtCheckBox.text=_Definition lists in the way of
214
MarkdownOptionsPane.definitionListsExtLink.text=Markdown Extra
215
MarkdownOptionsPane.extAnchorLinksExtCheckBox.text=Generate anchor links for headers using complete contents of the header
216
MarkdownOptionsPane.fencedCodeBlocksExtCheckBox.text=_Fenced Code Blocks in the way of
217
MarkdownOptionsPane.fencedCodeBlocksExtLabel.text=or
218
MarkdownOptionsPane.fencedCodeBlocksExtLink.text=Markdown Extra
219
MarkdownOptionsPane.fencedCodeBlocksExtLink2.text=Github-flavoured-Markdown
220
MarkdownOptionsPane.forceListItemParaExtCheckBox.text=Force List and Definition Paragraph wrapping if it includes more than just a single paragraph
221
MarkdownOptionsPane.hardwrapsExtCheckBox.text=_Newlines in paragraph-like content as real line breaks, see
222
MarkdownOptionsPane.hardwrapsExtLink.text=Github-flavoured-Markdown
223
MarkdownOptionsPane.quotesExtCheckBox.text=Beautify single _quotes, double quotes and double angle quotes (\u00ab and \u00bb)
224
MarkdownOptionsPane.relaxedHrRulesExtCheckBox.text=Allow horizontal rules without a blank line following them
225
MarkdownOptionsPane.smartsExtCheckBox.text=Beautify apostrophes, _ellipses ("..." and ". . .") and dashes ("--" and "---")
226
MarkdownOptionsPane.strikethroughExtCheckBox.text=_Strikethrough
227
MarkdownOptionsPane.suppressHtmlBlocksExtCheckBox.text=Suppress the _output of HTML blocks
228
MarkdownOptionsPane.suppressInlineHtmlExtCheckBox.text=Suppress the o_utput of inline HTML elements
229
MarkdownOptionsPane.tablesExtCheckBox.text=_Tables similar to
230
MarkdownOptionsPane.tablesExtLabel.text=(like
231
MarkdownOptionsPane.tablesExtLabel2.text=tables, but with colspan support)
232
MarkdownOptionsPane.tablesExtLink.text=MultiMarkdown
233
MarkdownOptionsPane.tablesExtLink2.text=Markdown Extra
234
MarkdownOptionsPane.taskListItemsExtCheckBox.text=GitHub style task list items
235
MarkdownOptionsPane.wikilinksExtCheckBox.text=_Wiki-style links ("[[wiki link]]")
1236
A src/main/resources/com/scrivenvar/preview/webview.css
1
/*
2
This software is released under the MIT license:
3
4
Permission is hereby granted, free of charge, to any person obtaining a copy of
5
this software and associated documentation files (the "Software"), to deal in
6
the Software without restriction, including without limitation the rights to
7
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
the Software, and to permit persons to whom the Software is furnished to do so,
9
subject to the following conditions:
10
11
The above copyright notice and this permission notice shall be included in all
12
copies or substantial portions of the Software.
13
14
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
17
COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
18
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
19
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
20
*/
21
22
/* Source: https://github.com/nicolashery/markdownpad-github */
23
24
/*  GitHub stylesheet for MarkdownPad (http://markdownpad.com) */
25
26
/* RESET
27
=============================================================================*/
28
29
html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6,
30
p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn,
31
em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var,
32
b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label,
33
legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas,
34
details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output,
35
ruby, section, summary, time, mark, audio, video {
36
  margin: 0;
37
  padding: 0;
38
  border: 0;
39
}
40
41
/* BODY
42
=============================================================================*/
43
44
body {
45
  font-family: serif;
46
  font-size: 14px;
47
  line-height: 1.6;
48
  color: #333;
49
  background-color: #fff;
50
  padding: 20px;
51
  max-width: 960px;
52
  margin: 0 auto;
53
}
54
55
body>*:first-child {
56
  margin-top: 0 !important;
57
}
58
59
body>*:last-child {
60
  margin-bottom: 0 !important;
61
}
62
63
/* BLOCKS
64
=============================================================================*/
65
66
p, blockquote, ul, ol, dl, table, pre {
67
  margin: 15px 0;
68
}
69
70
/* HEADERS
71
=============================================================================*/
72
73
h1, h2, h3, h4, h5, h6 {
74
  margin: 20px 0 10px;
75
  padding: 0;
76
  font-weight: bold;
77
  -webkit-font-smoothing: antialiased;
78
}
79
80
h1 tt, h1 code, h2 tt, h2 code, h3 tt, h3 code,
81
h4 tt, h4 code, h5 tt, h5 code, h6 tt, h6 code {
82
  font-size: inherit;
83
}
84
85
h1 {
86
  font-size: 28px;
87
  color: #000;
88
}
89
90
h2 {
91
  font-size: 24px;
92
  border-bottom: 1px solid #ccc;
93
  color: #000;
94
}
95
96
h3 {
97
  font-size: 18px;
98
}
99
100
h4 {
101
  font-size: 16px;
102
}
103
104
h5 {
105
  font-size: 14px;
106
}
107
108
h6 {
109
  color: #777;
110
  font-size: 14px;
111
}
112
113
body>h2:first-child, body>h1:first-child, body>h1:first-child+h2,
114
body>h3:first-child, body>h4:first-child, body>h5:first-child,
115
body>h6:first-child {
116
  margin-top: 0;
117
  padding-top: 0;
118
}
119
120
a:first-child h1, a:first-child h2, a:first-child h3,
121
a:first-child h4, a:first-child h5, a:first-child h6 {
122
  margin-top: 0;
123
  padding-top: 0;
124
}
125
126
h1+p, h2+p, h3+p, h4+p, h5+p, h6+p {
127
  margin-top: 10px;
128
}
129
130
/* LINKS
131
=============================================================================*/
132
133
a {
134
  color: #4183C4;
135
  text-decoration: none;
136
}
137
138
a:hover {
139
  text-decoration: underline;
140
}
141
142
/* LISTS
143
=============================================================================*/
144
145
ul, ol {
146
  padding-left: 30px;
147
}
148
149
ul li > :first-child, 
150
ol li > :first-child, 
151
ul li ul:first-of-type, 
152
ol li ol:first-of-type, 
153
ul li ol:first-of-type, 
154
ol li ul:first-of-type {
155
  margin-top: 0px;
156
}
157
158
ul ul, ul ol, ol ol, ol ul {
159
  margin-bottom: 0;
160
}
161
162
dl {
163
  padding: 0;
164
}
165
166
dl dt {
167
  font-size: 14px;
168
  font-weight: bold;
169
  font-style: italic;
170
  padding: 0;
171
  margin: 15px 0 5px;
172
}
173
174
dl dt:first-child {
175
  padding: 0;
176
}
177
178
dl dt>:first-child {
179
  margin-top: 0px;
180
}
181
182
dl dt>:last-child {
183
  margin-bottom: 0px;
184
}
185
186
dl dd {
187
  margin: 0 0 15px;
188
  padding: 0 15px;
189
}
190
191
dl dd>:first-child {
192
  margin-top: 0px;
193
}
194
195
dl dd>:last-child {
196
  margin-bottom: 0px;
197
}
198
199
/* CODE
200
=============================================================================*/
201
202
pre, code, tt {
203
  font-size: 12px;
204
  font-family: Consolas, "Liberation Mono", Courier, monospace;
205
}
206
207
code, tt {
208
  margin: 0 0px;
209
  padding: 0px 0px;
210
  white-space: nowrap;
211
  border: 1px solid #eaeaea;
212
  background-color: #f8f8f8;
213
  border-radius: 3px;
214
}
215
216
pre>code {
217
  margin: 0;
218
  padding: 0;
219
  white-space: pre;
220
  border: none;
221
  background: transparent;
222
}
223
224
pre {
225
  background-color: #f8f8f8;
226
  border: 1px solid #ccc;
227
  font-size: 13px;
228
  line-height: 19px;
229
  overflow: auto;
230
  padding: 6px 10px;
231
  border-radius: 3px;
232
}
233
234
pre code, pre tt {
235
  background-color: transparent;
236
  border: none;
237
}
238
239
kbd {
240
  -moz-border-bottom-colors: none;
241
  -moz-border-left-colors: none;
242
  -moz-border-right-colors: none;
243
  -moz-border-top-colors: none;
244
  background-color: #DDDDDD;
245
  background-image: linear-gradient(#F1F1F1, #DDDDDD);
246
  background-repeat: repeat-x;
247
  border-color: #DDDDDD #CCCCCC #CCCCCC #DDDDDD;
248
  border-image: none;
249
  border-radius: 2px 2px 2px 2px;
250
  border-style: solid;
251
  border-width: 1px;
252
  font-family: "Helvetica Neue",Helvetica,Arial,sans-serif;
253
  line-height: 10px;
254
  padding: 1px 4px;
255
}
256
257
/* QUOTES
258
=============================================================================*/
259
260
blockquote {
261
  border-left: 4px solid #DDD;
262
  padding: 0 15px;
263
  color: #777;
264
}
265
266
blockquote>:first-child {
267
  margin-top: 0px;
268
}
269
270
blockquote>:last-child {
271
  margin-bottom: 0px;
272
}
273
274
/* HORIZONTAL RULES
275
=============================================================================*/
276
277
hr {
278
  clear: both;
279
  margin: 15px 0;
280
  height: 0px;
281
  overflow: hidden;
282
  border: none;
283
  background: transparent;
284
  border-bottom: 4px solid #ddd;
285
  padding: 0;
286
}
287
288
/* TABLES
289
=============================================================================*/
290
291
table th {
292
  font-weight: bold;
293
}
294
295
table th, table td {
296
  border: 1px solid #ccc;
297
  padding: 6px 13px;
298
}
299
300
table tr {
301
  border-top: 1px solid #ccc;
302
  background-color: #fff;
303
}
304
305
table tr:nth-child(2n) {
306
  background-color: #f8f8f8;
307
}
308
309
/* IMAGES
310
=============================================================================*/
311
312
img {
313
  max-width: 100%
314
}
1315
A src/main/resources/com/scrivenvar/scene.css
1
/*
2
 * Copyright (c) 2015 Karl Tauber <karl at jformdesigner dot com>
3
 * All rights reserved.
4
 *
5
 * Redistribution and use in source and binary forms, with or without
6
 * modification, are permitted provided that the following conditions are met:
7
 *
8
 *  o Redistributions of source code must retain the above copyright
9
 *    notice, this list of conditions and the following disclaimer.
10
 *
11
 *  o Redistributions in binary form must reproduce the above copyright
12
 *    notice, this list of conditions and the following disclaimer in the
13
 *    documentation and/or other materials provided with the distribution.
14
 *
15
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
16
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
17
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
18
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
19
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
20
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
21
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
22
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
23
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
24
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
25
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
26
 */
27
28
/*---- toolbar ----*/
29
30
.tool-bar {
31
	-fx-spacing: 0;
32
}
33
34
.tool-bar .button {
35
	-fx-background-color: transparent;
36
}
37
38
.tool-bar .button:hover {
39
	-fx-background-color: -fx-shadow-highlight-color, -fx-outer-border, -fx-inner-border, -fx-body-color;
40
	-fx-color: -fx-hover-base;
41
}
42
43
.tool-bar .button:armed {
44
	-fx-color: -fx-pressed-base;
45
}
146
A src/main/resources/com/scrivenvar/settings.properties
1
# ########################################################################
2
# Application
3
# ########################################################################
4
5
application.title=scrivenvar
6
application.package=com/${application.title}
7
application.messages= com.${application.title}.messages
8
9
# Suppress multiple file modified notifications for one logical modification.
10
# Given in milliseconds.
11
application.watchdog.timeout=50
12
13
# ########################################################################
14
# Preferences
15
# ########################################################################
16
17
preferences.root=com.${application.title}
18
preferences.root.state=state
19
preferences.root.options=options
20
preferences.root.definition.source=definition.source
21
22
# ########################################################################
23
# File and Path References
24
# ########################################################################
25
file.stylesheet.scene=${application.package}/scene.css
26
file.stylesheet.markdown=${application.package}/editor/markdown.css
27
file.stylesheet.preview=webview.css
28
file.stylesheet.xml=${application.package}/xml.css
29
30
file.logo.16 =${application.package}/logo16.png
31
file.logo.32 =${application.package}/logo32.png
32
file.logo.128=${application.package}/logo128.png
33
file.logo.256=${application.package}/logo256.png
34
file.logo.512=${application.package}/logo512.png
35
36
# Default file name when a new file is created.
37
# This ensures that the file type can always be
38
# discerned so that the correct type of variable
39
# reference can be inserted.
40
file.default=untitled.md
41
file.definition.default=variables.yaml
42
43
# ########################################################################
44
# File name Extensions
45
# ########################################################################
46
47
# Comma-separated list of definition file name extensions.
48
definition.file.ext.json=*.json
49
definition.file.ext.toml=*.toml
50
definition.file.ext.yaml=*.yml,*.yaml
51
definition.file.ext.properties=*.properties,*.props
52
53
# Comma-separated list of file name extensions.
54
file.ext.rmarkdown=*.Rmd
55
file.ext.rxml=*.Rxml
56
file.ext.source=*.md,*.markdown,*.mkdown,*.mdown,*.mkdn,*.mkd,*.mdwn,*.mdtxt,*.mdtext,*.text,*.txt,${file.ext.rmarkdown},${file.ext.rxml}
57
file.ext.definition=${definition.file.ext.yaml}
58
file.ext.xml=*.xml,${file.ext.rxml}
59
file.ext.all=*.*
60
61
# File name extension search order for images.
62
file.ext.image.order=svg pdf png jpg tiff
63
64
# ########################################################################
65
# Variable Name Editor
66
# ########################################################################
67
68
# Maximum number of characters for a variable name. A variable is defined
69
# as one or more non-whitespace characters up to this maximum length.
70
editor.variable.maxLength=256
71
72
# ########################################################################
73
# Dialog Preferences
74
# ########################################################################
75
76
dialog.alert.button.order.mac=L_HE+U+FBIX_NCYOA_R
77
dialog.alert.button.order.linux=L_HE+UNYACBXIO_R
78
dialog.alert.button.order.windows=L_E+U+FBXI_YNOCAH_R
79
80
# Ensures a consistent button order for alert dialogs across platforms (because
81
# the default button order on Linux defies all logic).
82
dialog.alert.button.order=${dialog.alert.button.order.windows}
183
A src/main/resources/com/scrivenvar/variables.yaml
1
---
2
c:
3
  protagonist:
4
    name:
5
      First: Chloe
6
      First_pos: $c.protagonist.name.First$'s
7
      Middle: Irene
8
      Family: Angelos
9
      nick:
10
        Father: Savant
11
        Mother: Sweetie
12
    colour:
13
      eyes: green
14
      hair: dark auburn
15
      syn_1: black
16
      syn_2: purple
17
      syn_11: teal
18
      syn_6: silver
19
      favourite: emerald green
20
    speech:
21
      tic: oh
22
    father:
23
      heritage: Greek
24
      name:
25
        Short: Bryce
26
        First: Bryson
27
        First_pos: $c.protagonist.father.name.First$'s
28
        Honourific: Mr.
29
      education: Masters
30
      vocation:
31
        name: robotics
32
        title: roboticist
33
      employer:
34
        name:
35
          Short: Rabota
36
          Full: $c.protagonist.father.employer.name.Short$ Designs
37
      hair:
38
        style: thick, curly
39
        colour: black
40
      eyes:
41
        colour: dark brown
42
      Endear: Dad
43
      vehicle: coupé
44
    mother:
45
      name:
46
        Short: Cass
47
        First: Cassandra
48
        First_pos: $c.protagonist.mother.name.First$'s
49
        Honourific: Mrs.
50
      education: PhD
51
      speech:
52
        tic: cute
53
        Honorific: Doctor
54
      vocation:
55
        article: an
56
        name: oceanography
57
        title: oceanographer
58
      employer:
59
        name:
60
          Full: Oregon State University
61
          Short: OSU
62
      eyes:
63
        colour: blue
64
      hair:
65
        style: thick, curly
66
        colour: dark brown
67
      Endear: Mom
68
      Endear_pos: Mom's
69
    uncle:
70
      name:
71
        First: Damian
72
        First_pos: $c.protagonist.uncle.name.First$'s
73
        Family: Moros
74
      hands:
75
        fingers:
76
          shape: long, bony
77
    friend:
78
      primary:
79
        name:
80
          First: Gerard
81
          First_pos: $c.protagonist.friend.primary.name.First$'s
82
          Family: Baran
83
          Family_pos: $c.protagonist.friend.primary.name.Family$'s
84
        favourite:
85
          colour: midnight blue
86
        eyes:
87
          colour: hazel
88
        mother:
89
          name:
90
            First: Isabella
91
            Short: Izzy
92
            Honourific: Mrs.
93
        father:
94
          name:
95
            Short: Mo
96
            First: Montgomery
97
            First_pos: $c.protagonist.friend.primary.father.name.First$'s
98
            Honourific: Mr.
99
          speech:
100
            tic: y'know
101
          endear: Pops
102
  military:
103
    primary:
104
      name:
105
        First: Felix
106
        Family: LeMay
107
        Family_pos: LeMay's
108
      rank:
109
        Short: General
110
        Full: Brigadier $c.military.primary.rank.Short$
111
      colour:
112
        eyes: gray
113
        hair: dirty brown
114
    secondary:
115
      name:
116
        Family: Grell
117
      rank: Colonel
118
      colour:
119
        eyes: green
120
        hair: deep red
121
    quaternary:
122
      name:
123
        First: Gretchen
124
        Family: Steinherz
125
  minor:
126
    primary:
127
      name:
128
        First: River
129
        Family: Banks
130
        Honourific: Mx.
131
      vocation:
132
        title: salesperson
133
      employer:
134
        Name: Geophysical Prospecting Incorporated
135
        Abbr: GPI
136
        Area: Cold Spring Creek
137
        payment: twenty million
138
    secondary:
139
      name:
140
        First: Renato
141
        Middle: Carroña
142
        Family: Salvatierra
143
        Family_pos: $c.minor.secondary.name.Family$'s
144
        Full: $c.minor.secondary.name.First$ $c.minor.secondary.name.Middle$ Alejandro Gregorio Eduardo Salomón Vidal $c.minor.secondary.name.Family$
145
        Honourific: Mister
146
        Honourific_sp: Señor
147
      vocation:
148
        title: detective
149
    tertiary:
150
      name:
151
        First: Robert
152
        Family: Hanssen
153
154
  ai:
155
    protagonist:
156
      name:
157
        first: yoky
158
        First: Yoky
159
        First_pos: $c.ai.protagonist.name.First$'s
160
        Family: Tsukuda
161
        id: 46692
162
      persona:
163
        name:
164
          First: Hoshi
165
          First_pos: $c.ai.protagonist.persona.name.First$'s
166
          Family: Yamamoto
167
          Family_pos: $c.ai.protagonist.persona.name.Family$'s
168
      culture: Japanese-American
169
      ethnicity: Asian
170
      rank: Technical Sergeant
171
      speech:
172
        tic: okay
173
    first:
174
      Name: Prôtos
175
      Name_pos: Prôtos'
176
      age:
177
        actual: twenty-six weeks
178
        virtual: five years
179
    second:
180
      Name: Défteros
181
    third:
182
      Name: Trítos
183
    fourth:
184
      Name: Tétartos
185
    material:
186
      type: metal
187
      raw: ilmenite
188
      extract: ore
189
      name:
190
        short: titanium
191
        long: $c.ai.material.name.short$ dioxide
192
        Abbr: TiO~2~
193
      pejorative: tin
194
  animal:
195
    protagonist:
196
      Name: Trufflers
197
      type: pig
198
    antagonist:
199
      name: coywolf
200
      Name: Coywolf
201
      plural: coywolves
202
203
narrator:
204
  one: (by $c.protagonist.father.name.First$ $c.protagonist.name.Family$)
205
  two: (by $c.protagonist.mother.name.First$ $c.protagonist.name.Family$)
206
207
military:
208
  name:
209
    Short: Agency
210
    Short_pos: $military.name.Short$'s
211
    plural: agencies
212
  machine:
213
    Name: Skopós
214
    Name_pos: $military.machine.Name$'
215
    Location: Arctic
216
    predictor: quantum chips
217
  land:
218
    name:
219
      Full: $military.name.Short$ of Defence
220
    Slogan: Safety in Numbers
221
  air:
222
    name:
223
      Full: $military.name.Short$ of Air
224
  compound:
225
    type: base
226
    lights:
227
      colour: blue
228
    nick:
229
      Prefix: Catacombs
230
      prep: of
231
      Suffix: Tartarus
232
233
government:
234
  Country: United States
235
236
location:
237
  protagonist:
238
    City: Corvallis
239
    Region: Oregon
240
    Geography: Willamette Valley
241
    secondary:
242
      City: Willow Branch Spring
243
      Region: Oregon
244
      Geography: Wheeler County
245
      Water: Clarno Rapids
246
      Road: Shaniko-Fossil Highway
247
    tertiary:
248
      City: Leavenworth
249
      Region: Washington
250
      Type: Bavarian village
251
    school:
252
      address: 1400 Northwest Buchanan Avenue
253
    hospital:
254
      Name: Good Samaritan Regional Medical Center
255
  ai:
256
    escape:
257
      country:
258
        Name: Ecuador
259
        Name_pos: Ecuador's
260
      mountain:
261
        Name: Chimborazo
262
263
language:
264
  ai:
265
    article: an
266
    singular: exanimis
267
    plural: exanimēs
268
    brain:
269
      singular: superum
270
      plural: supera
271
    title: memristor array
272
    Title: Memristor Array
273
  police:
274
    slang:
275
      singular: mippo
276
      plural: $language.police.slang.singular$s
277
278
date:
279
  anchor: 2042-09-02
280
  protagonist:
281
    born: 0
282
    conceived: -243
283
    attacked:
284
      first: 2192
285
      second: 8064
286
    father:
287
      attacked:
288
        first: -8205
289
      date:
290
        second: -1550
291
    family:
292
      moved:
293
        first: $date.protagonist.conceived$ + 35
294
  game:
295
    played:
296
      first: $date.protagonist.born$ - 672
297
      second: $date.protagonist.family.moved.first$ + 2
298
  ai:
299
    interviewed: 6198
300
    onboarded: $date.ai.interviewed$ + 290
301
    diagnosed: $date.ai.onboarded$ + 2
302
    resigned: $date.ai.diagnosed$ + 3
303
    trapped: $date.ai.resigned$ + 26
304
    torturer: $date.ai.trapped$ + 18
305
    memristor: $date.ai.torturer$ + 61
306
    ethics: $date.ai.memristor$ + 415
307
    trained: $date.ai.ethics$ + 385
308
    mindjacked: $date.ai.trained$ + 22
309
    bombed: $date.ai.mindjacked$ + 458
310
  military:
311
    machine:
312
      Construction: Six years
313
314
plot:
315
  Log: $c.ai.protagonist.name.First_pos$ Chronicles
316
  Channel: Quantum Channel
317
318
  device:
319
    computer:
320
      Name: Tau
321
    network:
322
      Name: Internet
323
    paper:
324
      name:
325
        full: electronic sheet
326
        short: sheet
327
    typewriter:
328
      Name: Underwood
329
      year: nineteen twenties
330
      room: root cellar
331
    portable:
332
      name: nanobook
333
    vehicle:
334
      name: robocars
335
      Name: Robocars
336
    sensor:
337
      name: BMP1580
338
    phone:
339
      name: comm
340
      name_pos: $plot.device.phone.name$'s
341
      Name: Comm
342
      plural: $plot.device.phone.name$s
343
    video:
344
      name: vidfeed
345
      plural: $plot.device.video.name$s
346
    game:
347
      Name: Psynæris
348
      thought: transed
349
      machine: telecognos
350
      location:
351
        Building: Nijō Castle
352
        District: Gion
353
        City: Kyoto
354
        Country: Japan
355
356
farm:
357
  population:
358
    estimate: 350
359
    actual: 1,000
360
  energy: 9800kJ
361
  width: 55m
362
  length: 55m
363
  storeys: 10
364
365
lamp:
366
  height: 0.17m
367
  length: 1.22m
368
  width: 0.28m
369
370
crop:
371
  name: 
372
    singular: tomato
373
    plural: $crop.name.singular$es
374
  energy: 318kJ
375
  weight: 450g
376
  yield: 50
377
  harvests: 7
378
  diameter: 2m
379
  height: 1.5m
380
381
heading:
382
  ch_01: Till
383
  ch_02: Sow
384
  ch_03: Seed
385
  ch_04: Germinate
386
  ch_05: Grow
387
  ch_06: Shoot
388
  ch_07: Bud
389
  ch_08: Bloom
390
  ch_09: Pollinate
391
  ch_10: Fruit
392
  ch_11: Harvest
393
  ch_12: Deliver
394
  ch_13: Spoil
395
  ch_14: Revolt
396
  ch_15: Compost
397
  ch_16: Burn
398
  ch_17: Release
399
  ch_18: End Notes
400
  ch_19: Characters
401
402
inference:
403
  unit: per cent
404
  min: two
405
  ch_sow: eighty
406
  ch_seed: fifty-two
407
  ch_germinate: thirty-one
408
  ch_grow: fifteen
409
  ch_shoot: seven
410
  ch_bloom: four
411
  ch_pollinate: two
412
  ch_harvest: ninety-five
413
  ch_delivery: ninety-eight
414
415
link:
416
  tartarus: https://en.wikipedia.org/wiki/Tartarus
417
  exploits: https://www.google.ca/search?q=inurl:ftp+password+filetype:xls
418
  atalanta: https://en.wikipedia.org/wiki/Atalanta
419
  detain: https://goo.gl/RCNuOQ
420
  ceramics: https://en.wikipedia.org/wiki/Transparent_ceramics
421
  algernon: https://en.wikipedia.org/wiki/Flowers_for_Algernon
422
  holocaust: https://en.wikipedia.org/wiki/IBM_and_the_Holocaust
423
  memristor: http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.404.9037\&rep=rep1\&type=pdf
424
  surveillance: https://www.youtube.com/watch?v=XEVlyP4_11M#t=1487
425
  tor: https://www.torproject.org
426
  hydra: https://en.wikipedia.org/wiki/Lernaean_Hydra
427
  foliage: http://www.ncbi.nlm.nih.gov/pmc/articles/PMC3691134
428
  drake: http://www.bbc.com/future/story/20120821-how-many-alien-worlds-exist
429
  fermi: https://arxiv.org/pdf/1404.0204v1.pdf
430
  face: https://www.youtube.com/watch?v=ladqJQLR2bA
431
  expenditures: http://wikipedia.org/wiki/List_of_countries_by_military_expenditures
432
  governance: http://papers.ssrn.com/sol3/papers.cfm?abstract_id=2003531
433
  asimov: https://en.wikipedia.org/wiki/Three_Laws_of_Robotics
434
  clarke: https://en.wikipedia.org/wiki/Clarke's_three_laws
435
  jetpack: http://jetpackaviation.com/
436
  hoverboard: https://www.youtube.com/watch?v=WQzLrvz4DKQ
437
  eyes_five: https://en.wikipedia.org/wiki/Five_Eyes
438
  eyes_nine: https://www.privacytools.io/
439
  eyes_fourteen: http://electrospaces.blogspot.nl/2013/12/14-eyes-are-3rd-party-partners-forming.html
440
  tourism: http://www.spacefuture.com/archive/investigation_on_the_economic_and_technological_feasibiity_of_commercial_passenger_transportation_into_leo.shtml
441
1442
A src/main/resources/com/scrivenvar/xml.css
1
.tagmark {
2
    -fx-fill: gray;
3
}
4
.anytag {
5
    -fx-fill: crimson;
6
}
7
.paren {
8
    -fx-fill: firebrick;
9
    -fx-font-weight: bold;
10
}
11
.attribute {
12
    -fx-fill: darkviolet;
13
}
14
.avalue {
15
    -fx-fill: black;
16
}
117
18
.comment {
19
	-fx-fill: teal;
20
}
A src/test/java/com/scrivenvar/definition/TreeItemInterpolatorTest.java
1
/*
2
 * Copyright 2020 White Magic Software, Ltd.
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 *  o Redistributions of source code must retain the above copyright
10
 *    notice, this list of conditions and the following disclaimer.
11
 *
12
 *  o Redistributions in binary form must reproduce the above copyright
13
 *    notice, this list of conditions and the following disclaimer in the
14
 *    documentation and/or other materials provided with the distribution.
15
 *
16
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
17
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
18
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
19
 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
20
 * HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
21
 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
22
 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
23
 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
24
 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
25
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
26
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
27
 */
28
package com.scrivenvar.definition;
29
30
import javafx.scene.control.TreeItem;
31
import org.junit.jupiter.api.Test;
32
33
import static java.lang.String.format;
34
import static org.junit.jupiter.api.Assertions.assertEquals;
35
36
public class TreeItemInterpolatorTest {
37
38
  private final static String AUTHOR_FIRST = "FirstName";
39
  private final static String AUTHOR_LAST = "LastName";
40
  private final static String AUTHOR_ALL = "$root.name.first$ $root.name.last$";
41
42
  /**
43
   * Test that a hierarchical relationship of {@link TreeItem} instances can
44
   * create a flat map with all string values containing key names interpolated.
45
   */
46
  @Test
47
  public void test_Resolve_ReferencesInTree_InterpolatedMap() {
48
    final var root = new TreeItem<>( "root" );
49
    final var name = new TreeItem<>( "name" );
50
    final var first = new TreeItem<>( "first" );
51
    final var authorFirst = new TreeItem<>( AUTHOR_FIRST );
52
    final var last = new TreeItem<>( "last" );
53
    final var authorLast = new TreeItem<>( AUTHOR_LAST );
54
    final var full = new TreeItem<>( "full" );
55
    final var expr = new TreeItem<>( AUTHOR_ALL );
56
57
    root.getChildren().add( name );
58
    name.getChildren().add( first );
59
    name.getChildren().add( last );
60
    name.getChildren().add( full );
61
62
    first.getChildren().add( authorFirst );
63
    last.getChildren().add( authorLast );
64
    full.getChildren().add( expr );
65
66
    final var map = TreeItemAdapter.toMap( root );
67
68
    var actualAuthor = map.get( "$root.name.full$" );
69
    var expectedAuthor = AUTHOR_ALL;
70
    assertEquals( expectedAuthor, actualAuthor );
71
72
    MapInterpolator.interpolate( map );
73
    actualAuthor = map.get( "$root.name.full$" );
74
75
    expectedAuthor = format( "%s %s", AUTHOR_FIRST, AUTHOR_LAST );
76
    assertEquals( expectedAuthor, actualAuthor );
77
  }
78
}
179